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 }