go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/config/cfgclient/client.go (about)

     1  // Copyright 2020 The LUCI Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package cfgclient
    16  
    17  import (
    18  	"context"
    19  	"errors"
    20  	"net/http"
    21  	"strings"
    22  
    23  	"google.golang.org/grpc/credentials"
    24  
    25  	"go.chromium.org/luci/config"
    26  	"go.chromium.org/luci/config/impl/erroring"
    27  	"go.chromium.org/luci/config/impl/filesystem"
    28  	"go.chromium.org/luci/config/impl/remote"
    29  	"go.chromium.org/luci/config/impl/resolving"
    30  	"go.chromium.org/luci/config/vars"
    31  )
    32  
    33  // Options describe how to configure a LUCI Config client.
    34  type Options struct {
    35  	// Vars define how to substitute ${var} placeholders in config sets and paths.
    36  	//
    37  	// If nil, vars are not allowed. Pass &vars.Vars explicitly to use the global
    38  	// var set.
    39  	Vars *vars.VarSet
    40  
    41  	// ServiceHost is a hostname of a LUCI Config service to use.
    42  	//
    43  	// If given, indicates configs should be fetched from the LUCI Config service.
    44  	// If it's Config Service V1 (where the host ends with "appspot.com"), it
    45  	// requires ClientFactory to be provided as well.
    46  	//
    47  	// Not compatible with ConfigsDir.
    48  	ServiceHost string
    49  
    50  	// ConfigsDir is a file system directory to fetch configs from instead of
    51  	// a LUCI Config service.
    52  	//
    53  	// See https://godoc.org/go.chromium.org/luci/config/impl/filesystem for the
    54  	// expected layout of this directory.
    55  	//
    56  	// Useful when running locally in development mode. Not compatible with
    57  	// ServiceHost.
    58  	ConfigsDir string
    59  
    60  	// ClientFactory initializes an authenticating HTTP client on demand.
    61  	//
    62  	// It will be used to call LUCI Config service. Must be set if ServiceHost
    63  	// points to Config Service V1, ignored otherwise.
    64  	ClientFactory func(context.Context) (*http.Client, error)
    65  
    66  	// GetPerRPCCredsFn generates PerRPCCredentials for the gRPC connection.
    67  	//
    68  	// Must be set for calling Luci-Config v2, ignored otherwise.
    69  	GetPerRPCCredsFn func(context.Context) (credentials.PerRPCCredentials, error)
    70  
    71  	// UserAgent is the optional additional User-Agent fragment which will be
    72  	// appended to gRPC calls.
    73  	UserAgent string
    74  }
    75  
    76  // New instantiates a LUCI Config client based on the given options.
    77  //
    78  // The client fetches configs either from a LUCI Config service or from a local
    79  // directory on disk (e.g. when running locally in development mode), depending
    80  // on values of ServiceHost and ConfigsDir. If neither are set, returns a client
    81  // that fails all calls with an error.
    82  func New(ctx context.Context, opts Options) (config.Interface, error) {
    83  	switch {
    84  	case opts.ServiceHost == "" && opts.ConfigsDir == "":
    85  		return erroring.New(errors.New("LUCI Config client is not configured")), nil
    86  	case opts.ServiceHost != "" && opts.ConfigsDir != "":
    87  		return nil, errors.New("either a LUCI Config service or a local config directory should be used, not both")
    88  	case IsV1Host(opts.ServiceHost) && opts.ClientFactory == nil:
    89  		return nil, errors.New("need a client factory when using a LUCI Config service v1")
    90  	case opts.ServiceHost != "" && opts.GetPerRPCCredsFn == nil:
    91  		return nil, errors.New("GetPerRPCCredsFn must be set when using a LUCI Config service v2")
    92  	}
    93  
    94  	var base config.Interface
    95  	var err error
    96  	switch {
    97  	case opts.ServiceHost != "" && IsV1Host(opts.ServiceHost):
    98  		base = remote.NewV1(opts.ServiceHost, false, opts.ClientFactory)
    99  	case opts.ServiceHost != "":
   100  		var creds credentials.PerRPCCredentials
   101  		if creds, err = opts.GetPerRPCCredsFn(ctx); err == nil {
   102  			base, err = remote.NewV2(ctx, remote.V2Options{
   103  				Host:      opts.ServiceHost,
   104  				Creds:     creds,
   105  				UserAgent: opts.UserAgent,
   106  			})
   107  		}
   108  	case opts.ConfigsDir != "":
   109  		base, err = filesystem.New(opts.ConfigsDir)
   110  	default:
   111  		panic("impossible")
   112  	}
   113  	if err != nil {
   114  		return nil, err
   115  	}
   116  
   117  	varz := opts.Vars
   118  	if varz == nil {
   119  		varz = &vars.VarSet{} // empty: all var references will result in an error
   120  	}
   121  
   122  	return resolving.New(varz, base), nil
   123  }
   124  
   125  // IsV1Host checks if the provided host points to the old v1 service.
   126  func IsV1Host(host string) bool {
   127  	return strings.HasSuffix(host, ".appspot.com")
   128  }