go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/config_service/internal/service/update.go (about)

     1  // Copyright 2023 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 service
    16  
    17  import (
    18  	"context"
    19  	"errors"
    20  	"fmt"
    21  	"io"
    22  	"net/http"
    23  	"strings"
    24  	"sync"
    25  
    26  	"golang.org/x/sync/errgroup"
    27  	"google.golang.org/protobuf/encoding/protojson"
    28  	"google.golang.org/protobuf/proto"
    29  	"google.golang.org/protobuf/types/known/emptypb"
    30  
    31  	"go.chromium.org/luci/common/clock"
    32  	"go.chromium.org/luci/common/data/stringset"
    33  	"go.chromium.org/luci/common/logging"
    34  	cfgcommonpb "go.chromium.org/luci/common/proto/config"
    35  	"go.chromium.org/luci/common/sync/parallel"
    36  	"go.chromium.org/luci/config/validation"
    37  	"go.chromium.org/luci/gae/service/datastore"
    38  	"go.chromium.org/luci/gae/service/info"
    39  	"go.chromium.org/luci/grpc/prpc"
    40  	"go.chromium.org/luci/server/auth"
    41  
    42  	"go.chromium.org/luci/config_service/internal/common"
    43  	"go.chromium.org/luci/config_service/internal/model"
    44  )
    45  
    46  // Update updates the `Service` entities for all registered services.
    47  //
    48  // Also deletes the entities for un-registered services.
    49  func Update(ctx context.Context) error {
    50  	servicesCfg := &cfgcommonpb.ServicesCfg{}
    51  	if err := common.LoadSelfConfig(ctx, common.ServiceRegistryFilePath, servicesCfg); err != nil {
    52  		return fmt.Errorf("failed to load %s. Reason: %w", common.ServiceRegistryFilePath, err)
    53  	}
    54  
    55  	toDelete, err := computeServicesToDelete(ctx, servicesCfg)
    56  	if err != nil {
    57  		return err
    58  	}
    59  
    60  	var errs []error
    61  	var errorsMu sync.Mutex
    62  	perr := parallel.WorkPool(8, func(workC chan<- func() error) {
    63  		for _, srv := range servicesCfg.GetServices() {
    64  			srv := srv
    65  			workC <- func() error {
    66  				ctx := logging.SetField(ctx, "service", srv.GetId())
    67  				if err := updateService(ctx, srv); err != nil {
    68  					errorsMu.Lock()
    69  					errs = append(errs, err)
    70  					errorsMu.Unlock()
    71  					logging.Errorf(ctx, "failed to update service. Reason: %s", err)
    72  				}
    73  				return nil
    74  			}
    75  		}
    76  
    77  		if len(toDelete) > 0 {
    78  			workC <- func() error {
    79  				services := make([]string, len(toDelete))
    80  				for i, key := range toDelete {
    81  					services[i] = key.StringID()
    82  				}
    83  				if err := datastore.Delete(ctx, toDelete); err != nil {
    84  					errs = append(errs, fmt.Errorf("failed to delete service(s) [%s]: %w", strings.Join(services, ", "), err))
    85  					return nil
    86  				}
    87  				logging.Infof(ctx, "successfully deleted service(s): [%s]", strings.Join(services, ", "))
    88  				return nil
    89  			}
    90  		}
    91  	})
    92  
    93  	if perr != nil {
    94  		panic(fmt.Errorf("impossible pool error %w", perr))
    95  	}
    96  	return errors.Join(errs...)
    97  }
    98  
    99  func computeServicesToDelete(ctx context.Context, servicesCfg *cfgcommonpb.ServicesCfg) ([]*datastore.Key, error) {
   100  	var keys []*datastore.Key
   101  	if err := datastore.GetAll(ctx, datastore.NewQuery(model.ServiceKind).KeysOnly(true), &keys); err != nil {
   102  		return nil, fmt.Errorf("failed to query all service keys: %w", err)
   103  	}
   104  	currentServices := stringset.New(len(servicesCfg.GetServices()))
   105  	for _, srv := range servicesCfg.GetServices() {
   106  		currentServices.Add(srv.GetId())
   107  	}
   108  	toDelete := keys[:0] // reuse the memory of `keys`
   109  	for _, key := range keys {
   110  		if !currentServices.Has(key.StringID()) {
   111  			toDelete = append(toDelete, key)
   112  		}
   113  	}
   114  	return toDelete, nil
   115  }
   116  
   117  func updateService(ctx context.Context, srvInfo *cfgcommonpb.Service) error {
   118  	eg, ectx := errgroup.WithContext(ctx)
   119  	updated := &model.Service{
   120  		Name: srvInfo.GetId(),
   121  		Info: srvInfo,
   122  	}
   123  	switch {
   124  	case srvInfo.GetId() == info.AppID(ctx):
   125  		eg.Go(func() error {
   126  			metadata, err := getSelfMetadata(ctx)
   127  			if err != nil {
   128  				return err
   129  			}
   130  			if err := validateMetadata(metadata); err != nil {
   131  				return fmt.Errorf("invalid metadata for self service: %w", err)
   132  			}
   133  			updated.Metadata = metadata
   134  			return nil
   135  		})
   136  	case srvInfo.GetHostname() != "":
   137  		eg.Go(func() error {
   138  			metadata, err := fetchMetadata(ectx, srvInfo.GetHostname())
   139  			if err != nil {
   140  				return err
   141  			}
   142  			if err := validateMetadata(metadata); err != nil {
   143  				return fmt.Errorf("invalid metadata for service %s: %w", srvInfo.GetId(), err)
   144  			}
   145  			updated.Metadata = metadata
   146  			return nil
   147  		})
   148  	case srvInfo.GetMetadataUrl() != "":
   149  		eg.Go(func() error {
   150  			legacyMetadata, err := fetchLegacyMetadata(ectx, srvInfo.GetMetadataUrl(), srvInfo.GetJwtAuth().GetAudience())
   151  			if err != nil {
   152  				return err
   153  			}
   154  			if err := validateLegacyMetadata(legacyMetadata); err != nil {
   155  				return fmt.Errorf("invalid legacy metadata for service %s: %w", srvInfo.GetId(), err)
   156  			}
   157  			updated.LegacyMetadata = legacyMetadata
   158  			return nil
   159  		})
   160  	}
   161  
   162  	var existing *model.Service
   163  	eg.Go(func() error {
   164  		service := &model.Service{
   165  			Name: srvInfo.GetId(),
   166  		}
   167  		switch err := datastore.Get(ectx, service); err {
   168  		case datastore.ErrNoSuchEntity:
   169  			// Expect entity missing for the first time updating service.
   170  			logging.Warningf(ectx, "missing Service datastore entity for %q. This is common for first time updating service", srvInfo.GetId())
   171  		case nil:
   172  			existing = service
   173  		default:
   174  			return err
   175  		}
   176  		return nil
   177  	})
   178  	if err := eg.Wait(); err != nil {
   179  		return err
   180  	}
   181  
   182  	if skipUpdate(existing, updated) {
   183  		logging.Infof(ctx, "skip updating service as LUCI Config already has the latest")
   184  		return nil
   185  	}
   186  
   187  	updated.UpdateTime = clock.Now(ctx).UTC()
   188  	if err := datastore.Put(ctx, updated); err != nil {
   189  		return err
   190  	}
   191  	logging.Infof(ctx, "successfully updated service")
   192  	return nil
   193  }
   194  
   195  func fetchMetadata(ctx context.Context, endpoint string) (*cfgcommonpb.ServiceMetadata, error) {
   196  	tr, err := auth.GetRPCTransport(ctx, auth.AsSelf)
   197  	if err != nil {
   198  		return nil, fmt.Errorf("failed to create transport %w", err)
   199  	}
   200  	prpcClient := &prpc.Client{
   201  		C:    &http.Client{Transport: tr},
   202  		Host: endpoint,
   203  	}
   204  	if strings.HasPrefix(endpoint, "127.0.0.1") { // testing
   205  		prpcClient.Options = &prpc.Options{Insecure: true}
   206  	}
   207  	client := cfgcommonpb.NewConsumerClient(prpcClient)
   208  	return client.GetMetadata(ctx, &emptypb.Empty{})
   209  }
   210  
   211  func getSelfMetadata(ctx context.Context) (*cfgcommonpb.ServiceMetadata, error) {
   212  	patterns, err := validation.Rules.ConfigPatterns(ctx)
   213  	if err != nil {
   214  		return nil, fmt.Errorf("failed to collect config patterns from self rules %w", err)
   215  	}
   216  	ret := &cfgcommonpb.ServiceMetadata{
   217  		ConfigPatterns: make([]*cfgcommonpb.ConfigPattern, len(patterns)),
   218  	}
   219  	for i, p := range patterns {
   220  		ret.ConfigPatterns[i] = &cfgcommonpb.ConfigPattern{
   221  			ConfigSet: p.ConfigSet.String(),
   222  			Path:      p.Path.String(),
   223  		}
   224  	}
   225  	return ret, nil
   226  }
   227  
   228  func fetchLegacyMetadata(ctx context.Context, metadataURL string, jwtAud string) (*cfgcommonpb.ServiceDynamicMetadata, error) {
   229  	req, err := http.NewRequestWithContext(ctx, http.MethodGet, metadataURL, http.NoBody)
   230  	if err != nil {
   231  		return nil, fmt.Errorf("failed to create http request due to %w", err)
   232  	}
   233  	client := &http.Client{}
   234  	if jwtAud != "" {
   235  		if client.Transport, err = common.GetSelfSignedJWTTransport(ctx, jwtAud); err != nil {
   236  			return nil, err
   237  		}
   238  	} else {
   239  		if client.Transport, err = auth.GetRPCTransport(ctx, auth.AsSelf); err != nil {
   240  			return nil, fmt.Errorf("failed to create transport %w", err)
   241  		}
   242  	}
   243  	resp, err := client.Do(req)
   244  	if err != nil {
   245  		return nil, fmt.Errorf("failed to send request to %s due to %w", metadataURL, err)
   246  	}
   247  	defer func() { _ = resp.Body.Close() }()
   248  	switch body, err := io.ReadAll(resp.Body); {
   249  	case err != nil:
   250  		return nil, fmt.Errorf("failed to read the response from %s: %w", metadataURL, err)
   251  	case resp.StatusCode != http.StatusOK:
   252  		return nil, fmt.Errorf("%s returns %d. Body: %s", metadataURL, resp.StatusCode, body)
   253  	default:
   254  		ret := &cfgcommonpb.ServiceDynamicMetadata{}
   255  		if err := protojson.Unmarshal(body, ret); err != nil {
   256  			return nil, fmt.Errorf("failed to unmarshal ServiceDynamicMetadata: %w; Response body from %s: %s", err, metadataURL, body)
   257  		}
   258  		return ret, nil
   259  	}
   260  }
   261  
   262  func skipUpdate(existing, updated *model.Service) bool {
   263  	return existing != nil &&
   264  		existing.Name == updated.Name &&
   265  		proto.Equal(existing.Info, updated.Info) &&
   266  		proto.Equal(existing.Metadata, updated.Metadata) &&
   267  		proto.Equal(existing.LegacyMetadata, updated.LegacyMetadata)
   268  }