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  }