go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/config/impl/remote/remote.go (about) 1 // Copyright 2015 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 remote implements backends for config client which will make calls 16 // to the real Config Service. 17 package remote 18 19 import ( 20 "compress/zlib" 21 "context" 22 "encoding/base64" 23 "io" 24 "net/http" 25 "net/url" 26 "sort" 27 "strings" 28 29 "google.golang.org/api/googleapi" 30 31 configApi "go.chromium.org/luci/common/api/luci_config/config/v1" 32 "go.chromium.org/luci/common/errors" 33 "go.chromium.org/luci/common/logging" 34 "go.chromium.org/luci/common/retry/transient" 35 "go.chromium.org/luci/config" 36 ) 37 38 // ClientFactory returns HTTP client to use (given a context). 39 // 40 // See 'NewV1' for more details. 41 type ClientFactory func(context.Context) (*http.Client, error) 42 43 // NewV1 returns an implementation of the config service which talks to the 44 // actual luci-config service v1 using given transport. 45 // 46 // configServiceURL is usually "https://<host>/_ah/api/config/v1/". 47 // 48 // ClientFactory returns http.Clients to use for requests (given incoming 49 // contexts). It's required mostly to support GAE environment, where round 50 // trippers are bound to contexts and carry RPC deadlines. 51 // 52 // If 'clients' is nil, http.DefaultClient will be used for all requests. 53 func NewV1(host string, insecure bool, clients ClientFactory) config.Interface { 54 if clients == nil { 55 clients = func(context.Context) (*http.Client, error) { 56 return http.DefaultClient, nil 57 } 58 } 59 60 serviceURL := url.URL{ 61 Scheme: "https", 62 Host: host, 63 Path: "/_ah/api/config/v1/", 64 } 65 if insecure { 66 serviceURL.Scheme = "http" 67 } 68 69 return &remoteImpl{ 70 serviceURL: serviceURL.String(), 71 clients: clients, 72 } 73 } 74 75 type remoteImpl struct { 76 serviceURL string 77 clients ClientFactory 78 } 79 80 // service returns Cloud Endpoints API client bound to the given context. 81 // 82 // It inherits context's deadline and transport. 83 func (r *remoteImpl) service(ctx context.Context) (*configApi.Service, error) { 84 client, err := r.clients(ctx) 85 if err != nil { 86 return nil, err 87 } 88 service, err := configApi.New(client) 89 if err != nil { 90 return nil, err 91 } 92 93 service.BasePath = r.serviceURL 94 return service, nil 95 } 96 97 func (r *remoteImpl) GetConfig(ctx context.Context, configSet config.Set, path string, metaOnly bool) (*config.Config, error) { 98 srv, err := r.service(ctx) 99 if err != nil { 100 return nil, err 101 } 102 103 resp, err := srv.GetConfig(string(configSet), path).HashOnly(metaOnly).UseZlib(true).Context(ctx).Do() 104 if err != nil { 105 return nil, apiErr(err) 106 } 107 108 var decoded []byte 109 if !metaOnly { 110 if resp.IsZlibCompressed { 111 reader, err := zlib.NewReader(base64.NewDecoder(base64.StdEncoding, strings.NewReader(resp.Content))) 112 if err != nil { 113 return nil, err 114 } 115 if decoded, err = io.ReadAll(reader); err != nil { 116 return nil, err 117 } 118 } else { 119 if decoded, err = base64.StdEncoding.DecodeString(resp.Content); err != nil { 120 return nil, err 121 } 122 } 123 } 124 125 return &config.Config{ 126 Meta: config.Meta{ 127 ConfigSet: configSet, 128 Path: path, 129 ContentHash: resp.ContentHash, 130 Revision: resp.Revision, 131 ViewURL: resp.Url, 132 }, 133 Content: string(decoded), 134 }, nil 135 } 136 137 func (r *remoteImpl) GetConfigs(ctx context.Context, cfgSet config.Set, filter func(path string) bool, metaOnly bool) (map[string]config.Config, error) { 138 return nil, errors.New("this method is not supported when using v1 API") 139 } 140 141 func (r *remoteImpl) ListFiles(ctx context.Context, configSet config.Set) ([]string, error) { 142 srv, err := r.service(ctx) 143 if err != nil { 144 return nil, err 145 } 146 147 resp, err := srv.GetConfigSets().ConfigSet(string(configSet)).IncludeFiles(true).Context(ctx).Do() 148 if err != nil { 149 return nil, apiErr(err) 150 } 151 var files []string 152 for _, cs := range resp.ConfigSets { 153 for _, fs := range cs.Files { 154 files = append(files, fs.Path) 155 } 156 } 157 sort.Strings(files) 158 return files, nil 159 } 160 161 func (r *remoteImpl) GetProjects(ctx context.Context) ([]config.Project, error) { 162 srv, err := r.service(ctx) 163 if err != nil { 164 return nil, err 165 } 166 167 resp, err := srv.GetProjects().Context(ctx).Do() 168 if err != nil { 169 return nil, apiErr(err) 170 } 171 172 projects := make([]config.Project, len(resp.Projects)) 173 for i, p := range resp.Projects { 174 repoType := parseWireRepoType(p.RepoType) 175 176 url, err := url.Parse(p.RepoUrl) 177 if err != nil { 178 lc := logging.SetField(ctx, "projectID", p.Id) 179 logging.Warningf(lc, "Failed to parse repo URL %q: %s", p.RepoUrl, err) 180 } 181 182 projects[i] = config.Project{ 183 p.Id, 184 p.Name, 185 repoType, 186 url, 187 } 188 } 189 return projects, err 190 } 191 192 func (r *remoteImpl) Close() error { 193 return nil 194 } 195 196 func (r *remoteImpl) GetProjectConfigs(ctx context.Context, path string, metaOnly bool) ([]config.Config, error) { 197 srv, err := r.service(ctx) 198 if err != nil { 199 return nil, err 200 } 201 202 resp, err := srv.GetProjectConfigs(path).HashesOnly(metaOnly).Context(ctx).Do() 203 if err != nil { 204 return nil, apiErr(err) 205 } 206 207 c := logging.SetField(ctx, "path", path) 208 return convertMultiWireConfigs(c, path, resp, metaOnly) 209 } 210 211 // convertMultiWireConfigs is a utility to convert what we get over the wire 212 // into the structs we use in the config package. 213 func convertMultiWireConfigs(ctx context.Context, path string, wireConfigs *configApi.LuciConfigGetConfigMultiResponseMessage, metaOnly bool) ([]config.Config, error) { 214 configs := make([]config.Config, len(wireConfigs.Configs)) 215 for i, c := range wireConfigs.Configs { 216 var decoded []byte 217 var err error 218 219 if !metaOnly { 220 decoded, err = base64.StdEncoding.DecodeString(c.Content) 221 if err != nil { 222 lc := logging.SetField(ctx, "configSet", c.ConfigSet) 223 logging.Warningf(lc, "Failed to base64 decode config: %s", err) 224 } 225 } 226 227 configs[i] = config.Config{ 228 Meta: config.Meta{ 229 ConfigSet: config.Set(c.ConfigSet), 230 Path: path, 231 ContentHash: c.ContentHash, 232 Revision: c.Revision, 233 ViewURL: c.Url, 234 }, 235 Content: string(decoded), 236 Error: err, 237 } 238 } 239 240 return configs, nil 241 } 242 243 // parseWireRepoType parses the string received over the wire from 244 // the luci-config service that represents the repo type. 245 func parseWireRepoType(s string) config.RepoType { 246 if s == string(config.GitilesRepo) { 247 return config.GitilesRepo 248 } 249 250 return config.UnknownRepo 251 } 252 253 // apiErr converts googleapi.Error to an appropriate type. 254 func apiErr(e error) error { 255 err, ok := e.(*googleapi.Error) 256 if !ok { 257 return e 258 } 259 if err.Code == 404 { 260 return config.ErrNoConfig 261 } 262 if err.Code >= 500 { 263 return transient.Tag.Apply(err) 264 } 265 return err 266 }