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 }