blob: 089700bcf1c1be3dc93250f8f90db09b49e27f85 [file] [log] [blame]
// Copyright 2020 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 cfgclient
import (
"context"
"errors"
"net/http"
"strings"
"google.golang.org/grpc/credentials"
"go.chromium.org/luci/config"
"go.chromium.org/luci/config/impl/erroring"
"go.chromium.org/luci/config/impl/filesystem"
"go.chromium.org/luci/config/impl/remote"
"go.chromium.org/luci/config/impl/resolving"
"go.chromium.org/luci/config/vars"
)
// Options describe how to configure a LUCI Config client.
type Options struct {
// Vars define how to substitute ${var} placeholders in config sets and paths.
//
// If nil, vars are not allowed. Pass &vars.Vars explicitly to use the global
// var set.
Vars *vars.VarSet
// ServiceHost is a hostname of a LUCI Config service to use.
//
// If given, indicates configs should be fetched from the LUCI Config service.
// If it's Config Service V1 (where the host ends with "appspot.com"), it
// requires ClientFactory to be provided as well.
//
// Not compatible with ConfigsDir.
ServiceHost string
// ConfigsDir is a file system directory to fetch configs from instead of
// a LUCI Config service.
//
// See https://godoc.org/go.chromium.org/luci/config/impl/filesystem for the
// expected layout of this directory.
//
// Useful when running locally in development mode. Not compatible with
// ServiceHost.
ConfigsDir string
// ClientFactory initializes an authenticating HTTP client on demand.
//
// It will be used to call LUCI Config service. Must be set if ServiceHost
// points to Config Service V1, ignored otherwise.
ClientFactory func(context.Context) (*http.Client, error)
// GetPerRPCCredsFn generates PerRPCCredentials for the gRPC connection.
//
// Must be set for calling Luci-Config v2, ignored otherwise.
GetPerRPCCredsFn func(context.Context) (credentials.PerRPCCredentials, error)
// UserAgent is the optional additional User-Agent fragment which will be
// appended to gRPC calls.
UserAgent string
}
// New instantiates a LUCI Config client based on the given options.
//
// The client fetches configs either from a LUCI Config service or from a local
// directory on disk (e.g. when running locally in development mode), depending
// on values of ServiceHost and ConfigsDir. If neither are set, returns a client
// that fails all calls with an error.
func New(ctx context.Context, opts Options) (config.Interface, error) {
switch {
case opts.ServiceHost == "" && opts.ConfigsDir == "":
return erroring.New(errors.New("LUCI Config client is not configured")), nil
case opts.ServiceHost != "" && opts.ConfigsDir != "":
return nil, errors.New("either a LUCI Config service or a local config directory should be used, not both")
case IsV1Host(opts.ServiceHost) && opts.ClientFactory == nil:
return nil, errors.New("need a client factory when using a LUCI Config service v1")
case opts.ServiceHost != "" && opts.GetPerRPCCredsFn == nil:
return nil, errors.New("GetPerRPCCredsFn must be set when using a LUCI Config service v2")
}
var base config.Interface
var err error
switch {
case opts.ServiceHost != "" && IsV1Host(opts.ServiceHost):
base = remote.NewV1(opts.ServiceHost, false, opts.ClientFactory)
case opts.ServiceHost != "":
var creds credentials.PerRPCCredentials
if creds, err = opts.GetPerRPCCredsFn(ctx); err == nil {
base, err = remote.NewV2(ctx, remote.V2Options{
Host: opts.ServiceHost,
Creds: creds,
UserAgent: opts.UserAgent,
})
}
case opts.ConfigsDir != "":
base, err = filesystem.New(opts.ConfigsDir)
default:
panic("impossible")
}
if err != nil {
return nil, err
}
varz := opts.Vars
if varz == nil {
varz = &vars.VarSet{} // empty: all var references will result in an error
}
return resolving.New(varz, base), nil
}
// IsV1Host checks if the provided host points to the old v1 service.
func IsV1Host(host string) bool {
return strings.HasSuffix(host, ".appspot.com")
}