go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/appengine/gaemiddleware/settings.go (about) 1 // Copyright 2016 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 gaemiddleware 16 17 import ( 18 "context" 19 "fmt" 20 "strings" 21 22 "go.chromium.org/luci/common/logging" 23 "go.chromium.org/luci/common/tsmon" 24 "go.chromium.org/luci/common/tsmon/metric" 25 mc "go.chromium.org/luci/gae/service/memcache" 26 "go.chromium.org/luci/server/portal" 27 "go.chromium.org/luci/server/settings" 28 ) 29 30 // settingsKey is key for global GAE settings (described by gaeSettings struct) 31 // in the settings store. See go.chromium.org/luci/server/settings. 32 const settingsKey = "gae" 33 34 // gaeSettings contain global Appengine related tweaks. They are stored in app 35 // settings store (based on the datastore, see appengine/gaesettings module) 36 // under settingsKey key. 37 type gaeSettings struct { 38 // LoggingLevel is logging level to set the default logger to. 39 // 40 // Log entries below this level will be completely ignored. They won't even 41 // reach GAE logging service. Default is logging.Debug (all logs hit logging 42 // service). 43 LoggingLevel logging.Level `json:"logging_level"` 44 45 // DisableDSCache is true to disable dscache (the memcache layer on top of 46 // the datastore). 47 DisableDSCache portal.YesOrNo `json:"disable_dscache"` 48 49 // SimulateMemcacheOutage is true to make every memcache call fail. 50 // 51 // Useful to verify apps can survive a memcache outage. 52 SimulateMemcacheOutage portal.YesOrNo `json:"simulate_memcache_outage"` 53 54 // EncryptionKey is a "sm://<project>/<secret>" path to a AEAD encryption key 55 // used to encrypt cookies and other sensitive things. 56 EncryptionKey string `json:"encryption_key"` 57 } 58 59 // fetchCachedSettings fetches gaeSettings from the settings store or panics. 60 // 61 // Uses in-process global cache to avoid hitting datastore often. The cache 62 // expiration time is 1 min (see gaesettings.expirationTime), meaning 63 // the instance will refetch settings once a minute (blocking only one unlucky 64 // request to do so). 65 // 66 // Panics only if there's no cached value (i.e. it is the first call to this 67 // function in this process ever) and datastore operation fails. It is a good 68 // idea to implement /_ah/warmup to warm this up. 69 func fetchCachedSettings(ctx context.Context) gaeSettings { 70 s := gaeSettings{} 71 switch err := settings.Get(ctx, settingsKey, &s); { 72 case err == nil: 73 return s 74 case err == settings.ErrNoSettings: 75 // Defaults. 76 return gaeSettings{LoggingLevel: logging.Debug} 77 default: 78 panic(fmt.Errorf("could not fetch GAE settings - %s", err)) 79 } 80 } 81 82 // dsCacheDisabled is a metric for reporting the value of DSCacheDisabled in 83 // gaeSettings. 84 var dsCacheDisabled = metric.NewBool( 85 "appengine/settings/dscache_disabled", 86 "Whether or not dscache is disabled in the admin portal.", 87 nil, 88 ) 89 90 // reportDSCacheDisabled reports the value of DSCacheDisabled in settings to 91 // tsmon. 92 func reportDSCacheDisabled(ctx context.Context) { 93 dsCacheDisabled.Set(ctx, bool(fetchCachedSettings(ctx).DisableDSCache)) 94 } 95 96 //////////////////////////////////////////////////////////////////////////////// 97 // UI for GAE settings. 98 99 type settingsPage struct { 100 portal.BasePage 101 } 102 103 func (settingsPage) Title(ctx context.Context) (string, error) { 104 return "Appengine related settings", nil 105 } 106 107 func (settingsPage) Fields(ctx context.Context) ([]portal.Field, error) { 108 return []portal.Field{ 109 { 110 ID: "LoggingLevel", 111 Title: "Minimal logging level", 112 Type: portal.FieldChoice, 113 ChoiceVariants: []string{ 114 "debug", 115 "info", 116 "warning", 117 "error", 118 }, 119 Validator: func(v string) error { 120 var l logging.Level 121 return l.Set(v) 122 }, 123 Help: `Log entries below this level will be <b>completely</b> ignored. 124 They won't even reach GAE logging service.`, 125 }, 126 portal.YesOrNoField(portal.Field{ 127 ID: "DisableDSCache", 128 Title: "Disable datastore cache", 129 Help: `Usually caching is a good thing and it can be left enabled. You may 130 want to disable it if memcache is having issues that prevent entity writes to 131 succeed. See <a href="https://godoc.org/go.chromium.org/luci/gae/filter/dscache"> 132 dscache documentation</a> for more information. Toggling this on and off has 133 consequences: <b>memcache is completely flushed</b>. Do not toy with this 134 setting.`, 135 }), 136 portal.YesOrNoField(portal.Field{ 137 ID: "SimulateMemcacheOutage", 138 Title: "Simulate memcache outage", 139 Help: `<b>Intended for development only. Do not use in production 140 applications.</b> When Yes, all memcache calls will fail, as if the memcache 141 service is unavailable. This is useful to test how application behaves when a 142 real memcache outage happens.`, 143 }), 144 { 145 ID: "EncryptionKey", 146 Title: "Encryption key URI", 147 Type: portal.FieldText, 148 Validator: func(v string) error { 149 switch { 150 case v == "": 151 return nil 152 case !strings.HasPrefix(v, "sm://"): 153 return fmt.Errorf("expecting an sm://... URI") 154 case strings.Count(strings.TrimPrefix(v, "sm://"), "/") != 1: 155 return fmt.Errorf("sm://... URI should have form sm://[project]/[secret]") 156 } 157 return nil 158 }, 159 Help: `An <code>sm://[project]/[secret]</code> URI pointing to an existing 160 secret in Google Secret Manager to use as an encryption key for encrypting 161 cookies and other sensitive strings.`, 162 }, 163 }, nil 164 } 165 166 func (settingsPage) ReadSettings(ctx context.Context) (map[string]string, error) { 167 s := gaeSettings{} 168 err := settings.GetUncached(ctx, settingsKey, &s) 169 if err != nil && err != settings.ErrNoSettings { 170 return nil, err 171 } 172 return map[string]string{ 173 "LoggingLevel": s.LoggingLevel.String(), 174 "DisableDSCache": s.DisableDSCache.String(), 175 "SimulateMemcacheOutage": s.SimulateMemcacheOutage.String(), 176 "EncryptionKey": s.EncryptionKey, 177 }, nil 178 } 179 180 func (settingsPage) WriteSettings(ctx context.Context, values map[string]string) error { 181 modified := gaeSettings{} 182 if err := modified.LoggingLevel.Set(values["LoggingLevel"]); err != nil { 183 return err 184 } 185 if err := modified.DisableDSCache.Set(values["DisableDSCache"]); err != nil { 186 return err 187 } 188 if err := modified.SimulateMemcacheOutage.Set(values["SimulateMemcacheOutage"]); err != nil { 189 return err 190 } 191 modified.EncryptionKey = values["EncryptionKey"] 192 193 // When switching dscache back on, wipe memcache. 194 existing := gaeSettings{} 195 err := settings.GetUncached(ctx, settingsKey, &existing) 196 if err != nil && err != settings.ErrNoSettings { 197 return err 198 } 199 if existing.DisableDSCache && !modified.DisableDSCache { 200 logging.Warningf(ctx, "DSCache was reenabled, flushing memcache") 201 if err := mc.Flush(ctx); err != nil { 202 return fmt.Errorf("failed to flush memcache after reenabling dscache - %s", err) 203 } 204 } 205 206 return settings.SetIfChanged(ctx, settingsKey, &modified) 207 } 208 209 func init() { 210 portal.RegisterPage(settingsKey, settingsPage{}) 211 tsmon.RegisterGlobalCallback(reportDSCacheDisabled, dsCacheDisabled) 212 }