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  }