go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/luci_notify/config/settings.go (about) 1 // Copyright 2017 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 21 "github.com/golang/protobuf/proto" 22 23 "go.chromium.org/luci/common/errors" 24 "go.chromium.org/luci/common/logging" 25 "go.chromium.org/luci/config/cfgclient" 26 "go.chromium.org/luci/config/validation" 27 "go.chromium.org/luci/gae/service/datastore" 28 29 notifypb "go.chromium.org/luci/luci_notify/api/config" 30 ) 31 32 // Settings represents the luci-notify configuration for a single instance of the service. 33 type Settings struct { 34 // ID is the datastore ID for service settings. 35 ID string `gae:"$id,service_config"` 36 37 // Revision is the revision of this instance's configuration. 38 Revision string 39 40 // Settings is an embedded copy of this instance's configuration proto. 41 Settings notifypb.Settings `gae:"-"` 42 } 43 44 // Load loads a Settings's information from props. 45 // 46 // This implements PropertyLoadSaver. Load unmarshals the property Settings 47 // stored in the datastore as a binary proto into the struct's Settings field. 48 func (s *Settings) Load(props datastore.PropertyMap) error { 49 if pdata, ok := props["Settings"]; ok { 50 settings := pdata.Slice() 51 if len(settings) != 1 { 52 return fmt.Errorf("property `Settings` is a property slice") 53 } 54 settingsBytes, ok := settings[0].Value().([]byte) 55 if !ok { 56 return fmt.Errorf("expected byte array for property `Settings`") 57 } 58 if err := proto.Unmarshal(settingsBytes, &s.Settings); err != nil { 59 return err 60 } 61 delete(props, "Settings") 62 } 63 return datastore.GetPLS(s).Load(props) 64 } 65 66 // Save saves a Settings's information to a property map. 67 // 68 // This implements PropertyLoadSaver. Save marshals the Settings 69 // field as a binary proto and stores it in the Settings property. 70 func (s *Settings) Save(withMeta bool) (datastore.PropertyMap, error) { 71 props, err := datastore.GetPLS(s).Save(withMeta) 72 if err != nil { 73 return nil, err 74 } 75 settingsBytes, err := proto.Marshal(&s.Settings) 76 if err != nil { 77 return nil, err 78 } 79 props["Settings"] = datastore.MkProperty(settingsBytes) 80 return props, nil 81 } 82 83 // updateSettings fetches the service config from luci-config and then stores 84 // the new config into the datastore. 85 func updateSettings(c context.Context) error { 86 // Load the settings from luci-config. 87 lucicfg := cfgclient.Client(c) 88 cfg, err := lucicfg.GetConfig(c, "services/${appid}", "settings.cfg", false) 89 if err != nil { 90 return errors.Annotate(err, "loading settings.cfg from luci-config").Err() 91 } 92 93 // Do the revision check & swap in a datastore transaction. 94 return datastore.RunInTransaction(c, func(c context.Context) error { 95 oldSettings := Settings{} 96 err := datastore.Get(c, &oldSettings) 97 switch err { 98 case datastore.ErrNoSuchEntity: 99 // This might be the first time this has run, so a warning here is ok. 100 logging.WithError(err).Warningf(c, "no existing service config") 101 case nil: 102 // Continue 103 default: 104 return errors.Annotate(err, "loading existing config").Err() 105 } 106 // Check to see if we need to update 107 if oldSettings.Revision == cfg.Revision { 108 logging.Debugf(c, "revisions matched (%s), no need to update", cfg.Revision) 109 return nil 110 } 111 newSettings := Settings{Revision: cfg.Revision} 112 if err := proto.UnmarshalText(cfg.Content, &newSettings.Settings); err != nil { 113 return errors.Annotate(err, "unmarshalling proto").Err() 114 } 115 ctx := &validation.Context{Context: c} 116 ctx.SetFile("settings.cfg") 117 validateSettings(ctx, &newSettings.Settings) 118 if err := ctx.Finalize(); err != nil { 119 return errors.Annotate(err, "validating settings").Err() 120 } 121 return datastore.Put(c, &newSettings) 122 }, nil) 123 } 124 125 func FetchSettings(ctx context.Context) (*notifypb.Settings, error) { 126 settings := Settings{} 127 err := datastore.Get(ctx, &settings) 128 if err != nil { 129 return nil, errors.Annotate(err, "loading existing config").Err() 130 } 131 return &settings.Settings, nil 132 }