go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/milo/internal/config/config.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 config 16 17 import ( 18 "context" 19 "fmt" 20 "time" 21 22 "google.golang.org/protobuf/proto" 23 24 "go.chromium.org/luci/common/errors" 25 "go.chromium.org/luci/common/logging" 26 protoutil "go.chromium.org/luci/common/proto" 27 configInterface "go.chromium.org/luci/config" 28 "go.chromium.org/luci/config/cfgclient" 29 "go.chromium.org/luci/gae/service/datastore" 30 configpb "go.chromium.org/luci/milo/proto/config" 31 "go.chromium.org/luci/server/caching" 32 ) 33 34 const ( 35 // serviceConfigID is the key for the service config entity in datastore. 36 serviceConfigID = "service_config" 37 38 // globalConfigFilename is the name for milo's configuration file on 39 // luci-config. 40 globalConfigFilename = "settings.cfg" 41 ) 42 43 // ServiceConfig is a container for the instance's service config. 44 type ServiceConfig struct { 45 // ID is the datastore key. This should be static, as there should only be 46 // one service config. 47 ID string `gae:"$id"` 48 // Revision is the revision of the config, taken from luci-config. This is used 49 // to determine if the entry needs to be refreshed. 50 Revision string 51 // Data is the binary proto of the config. 52 Data []byte `gae:",noindex"` 53 // Text is the text format of the config. For human consumption only. 54 Text string `gae:",noindex"` 55 // LastUpdated is the time this config was last updated. 56 LastUpdated time.Time 57 } 58 59 // GetSettings returns the service (aka global) config for the current 60 // instance of Milo from the datastore. Returns an empty config and warn heavily 61 // if none is found. 62 // TODO(hinoka): Use process cache to cache configs. 63 func GetSettings(c context.Context) *configpb.Settings { 64 settings := configpb.Settings{} 65 66 msg, err := GetCurrentServiceConfig(c) 67 if err != nil { 68 // The service config does not exist, just return an empty config 69 // and complain loudly in the logs. 70 logging.WithError(err).Errorf(c, 71 "Encountered error while loading service config, using empty config.") 72 return &settings 73 } 74 75 err = proto.Unmarshal(msg.Data, &settings) 76 if err != nil { 77 // The service config is broken, just return an empty config 78 // and complain loudly in the logs. 79 logging.WithError(err).Errorf(c, 80 "Encountered error while unmarshalling service config, using empty config.") 81 // Zero out the message just in case something got written in. 82 settings = configpb.Settings{} 83 } 84 85 return &settings 86 } 87 88 var serviceCfgCache = caching.RegisterCacheSlot() 89 90 // GetCurrentServiceConfig gets the service config for the instance from either 91 // process cache or datastore cache. 92 func GetCurrentServiceConfig(c context.Context) (*ServiceConfig, error) { 93 // This maker function is used to do the actual fetch of the ServiceConfig 94 // from datastore. It is called if the ServiceConfig is not in proc cache. 95 item, err := serviceCfgCache.Fetch(c, func(any) (any, time.Duration, error) { 96 msg := ServiceConfig{ID: serviceConfigID} 97 err := datastore.Get(c, &msg) 98 if err != nil { 99 return nil, time.Minute, err 100 } 101 logging.Infof(c, "loaded service config from datastore") 102 return msg, time.Minute, nil 103 }) 104 if err != nil { 105 return nil, fmt.Errorf("failed to get service config: %s", err.Error()) 106 } 107 if msg, ok := item.(ServiceConfig); ok { 108 logging.Infof(c, "loaded config entry from %s", msg.LastUpdated.Format(time.RFC3339)) 109 return &msg, nil 110 } 111 return nil, fmt.Errorf("could not load service config %#v", item) 112 } 113 114 // UpdateServiceConfig fetches the service config from luci-config 115 // and then stores a snapshot of the configuration in datastore. 116 func UpdateServiceConfig(c context.Context) (*configpb.Settings, error) { 117 // Acquire the raw config client. 118 content := "" 119 meta := configInterface.Meta{} 120 err := cfgclient.Get(c, 121 "services/${appid}", 122 globalConfigFilename, 123 cfgclient.String(&content), 124 &meta, 125 ) 126 if err != nil { 127 return nil, fmt.Errorf("could not load %s from luci-config: %s", globalConfigFilename, err) 128 } 129 130 // Reserialize it into a binary proto to make sure older/newer Milo versions 131 // can safely use the entity when some fields are added/deleted. Text protos 132 // do not guarantee that. 133 settings := &configpb.Settings{} 134 err = protoutil.UnmarshalTextML(content, settings) 135 if err != nil { 136 return nil, fmt.Errorf( 137 "could not unmarshal proto from luci-config:\n%s", content) 138 } 139 newConfig := ServiceConfig{ 140 ID: serviceConfigID, 141 Text: content, 142 Revision: meta.Revision, 143 LastUpdated: time.Now().UTC(), 144 } 145 newConfig.Data, err = proto.Marshal(settings) 146 if err != nil { 147 return nil, fmt.Errorf("could not marshal proto into binary\n%s", newConfig.Text) 148 } 149 150 // Do the revision check & swap in a datastore transaction. 151 err = datastore.RunInTransaction(c, func(c context.Context) error { 152 oldConfig := ServiceConfig{ID: serviceConfigID} 153 err := datastore.Get(c, &oldConfig) 154 switch err { 155 case datastore.ErrNoSuchEntity: 156 // Might be the first time this has run. 157 logging.WithError(err).Warningf(c, "No existing service config.") 158 case nil: 159 // Continue 160 default: 161 return fmt.Errorf("could not load existing config: %s", err) 162 } 163 // Check to see if we need to update 164 if oldConfig.Revision == newConfig.Revision { 165 logging.Infof(c, "revisions matched (%s), no need to update", oldConfig.Revision) 166 return nil 167 } 168 logging.Infof(c, "revisions differ (old %s, new %s), updating", 169 oldConfig.Revision, newConfig.Revision) 170 return datastore.Put(c, &newConfig) 171 }, nil) 172 173 if err != nil { 174 return nil, errors.Annotate(err, "failed to update config entry in transaction").Err() 175 } 176 logging.Infof(c, "successfully updated to new config") 177 178 return settings, nil 179 } 180 181 // UpdateConfigHandler is an HTTP handler that handles configuration update 182 // requests. 183 func UpdateConfigHandler(c context.Context) error { 184 _, err := UpdateServiceConfig(c) 185 if err != nil { 186 return errors.Annotate(err, "service update handler encountered error").Err() 187 } 188 189 return nil 190 }