blob: 9629d0b5397d05e8d076b7130f64e88e7c17e4c6 [file] [log] [blame]
// Copyright 2015 The LUCI Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package cli
import (
"context"
"flag"
"fmt"
"net/http"
"os"
"os/signal"
"strings"
"time"
"github.com/maruel/subcommands"
"go.chromium.org/luci/auth"
"go.chromium.org/luci/auth/client/authcli"
"go.chromium.org/luci/common/cli"
"go.chromium.org/luci/common/clock/clockflag"
"go.chromium.org/luci/common/errors"
log "go.chromium.org/luci/common/logging"
"go.chromium.org/luci/common/logging/gologger"
"go.chromium.org/luci/config"
"go.chromium.org/luci/grpc/prpc"
"go.chromium.org/luci/logdog/client/coordinator"
"go.chromium.org/luci/logdog/common/types"
)
func init() {
prpc.DefaultUserAgent = "logdog CLI"
}
// Parameters is the set of application parametesr that can be supplied.
type Parameters struct {
// Args is the command-line arguments. This should not include the command
// name, args[0].
Args []string
// Host is the default host name.
Host string
// DefaultAuthOptions provide default values for authentication related
// options (most notably SecretsDir: a directory with token cache).
DefaultAuthOptions auth.Options
}
type application struct {
cli.Application
context.Context
// p is the set of runtime parameters to use.
p Parameters
// project is the project name. This may either be a valid project name or
// empty. Subcommands that support "unified" project-in-path paths should use
// splitPath to get the project form the path. Those that don't should assert
// that this is non-empty.
project string
authFlags authcli.Flags
insecure bool
timeout clockflag.Duration
// httpClient is an authenticated HTTP client to use for connections to the
// Coordinator.
httpClient *http.Client
}
func (a *application) addToFlagSet(ctx context.Context, fs *flag.FlagSet) {
fs.StringVar(&a.p.Host, "host", a.p.Host,
"The LogDog Coordinator [host][:port].")
fs.BoolVar(&a.insecure, "insecure", false,
"Use insecure transport for RPC.")
fs.StringVar(&a.project, "project", "",
"The log stream's project.")
fs.Var(&a.timeout, "timeout",
"If >0, a duration string for the maximum amount of time to wait for a log entry "+
"before exiting with a 2. "+clockflag.DurationHelp)
}
// splitPath converts between a possible user-facing "unified" stream path
// (e.g., "project/path...") to separate project/path values.
//
// If a project is supplied via command-line, the path is returned directly
// along with the project. If no project is supplied, the first slash-delimited
// component of "p" is used as the project name.
func (a *application) splitPath(p string) (string, string, bool, error) {
if a.project != "" {
return a.project, p, false, nil
}
parts := strings.SplitN(p, types.StreamNameSepStr, 2)
project := parts[0]
if err := config.ValidateProjectName(project); err != nil {
return "", "", false, fmt.Errorf("invalid project name %q: %v", project, err)
}
if len(parts) == 2 {
p = parts[1]
} else {
p = ""
}
return project, p, true, nil
}
func (a *application) resolveHost(host string) (string, error) {
switch {
case host != "":
return host, nil
case a.p.Host != "":
return a.p.Host, nil
default:
return "", errors.New("a Coordinator host must be specified (-host)")
}
}
// coordinatorClient returns a Coordinator client for the specified host. If
// no host is provided, the command-line host will be used.
func (a *application) coordinatorClient(host string) (*coordinator.Client, error) {
host, err := a.resolveHost(host)
if err != nil {
return nil, errors.Annotate(err, "").Err()
}
// Get our Coordinator client instance.
prpcClient := prpc.Client{
C: a.httpClient,
Host: host,
Options: prpc.DefaultOptions(),
}
prpcClient.Options.Insecure = a.insecure
return coordinator.NewClient(&prpcClient), nil
}
func (a *application) timeoutCtx(c context.Context) (context.Context, context.CancelFunc) {
if a.timeout <= 0 {
return context.WithCancel(c)
}
return context.WithTimeout(c, time.Duration(a.timeout))
}
// Main is the entry point for the CLI application.
func Main(ctx context.Context, params Parameters) int {
ctx = gologger.StdConfig.Use(ctx)
authOptions := params.DefaultAuthOptions
authOptions.Scopes = coordinator.Scopes
a := application{
Application: cli.Application{
Name: "logdog",
Title: "LogDog log data access CLI",
Context: func(context.Context) context.Context { return ctx },
Commands: []*subcommands.Command{
subcommands.CmdHelp,
newCatCommand(),
newQueryCommand(),
newLatestCommand(),
authcli.SubcommandLogin(authOptions, "auth-login", false),
authcli.SubcommandLogout(authOptions, "auth-logout", false),
authcli.SubcommandInfo(authOptions, "auth-info", false),
},
},
p: params,
authFlags: authcli.Flags{},
}
loggingConfig := log.Config{
Level: log.Level(log.Info),
}
flags := &flag.FlagSet{}
a.addToFlagSet(ctx, flags)
loggingConfig.AddFlags(flags)
a.authFlags.Register(flags, authOptions)
// Parse flags.
if err := flags.Parse(params.Args); err != nil {
log.Errorf(log.SetError(ctx, err), "Failed to parse command-line.")
return 1
}
// Install our log formatter.
ctx = loggingConfig.Set(ctx)
// Signal handler will cancel our context when interrupted.
ctx, cancelFunc := context.WithCancel(ctx)
signalC := make(chan os.Signal, 1)
signal.Notify(signalC, os.Interrupt, os.Kill)
go func() {
triggered := false
for sig := range signalC {
if triggered {
os.Exit(2)
}
triggered = true
log.Fields{
"signal": sig,
}.Warningf(ctx, "Caught signal; terminating.")
cancelFunc()
}
}()
defer func() {
signal.Stop(signalC)
close(signalC)
}()
// Instantiate our authenticated HTTP client.
authOpts, err := a.authFlags.Options()
if err != nil {
log.Errorf(log.SetError(ctx, err), "Failed to create auth options.")
return 1
}
if a.httpClient, err = auth.NewAuthenticator(ctx, auth.OptionalLogin, authOpts).Client(); err != nil {
log.Errorf(log.SetError(ctx, err), "Failed to create authenticated client.")
return 1
}
a.Context = ctx
return subcommands.Run(&a, flags.Args())
}