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  }