go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/config/vars/vars.go (about)

     1  // Copyright 2020 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 vars implements a registry of ${var} placeholders.
    16  //
    17  // They can be used in config validation rule patterns and config set names
    18  // and paths in place of a not-yet-known values.
    19  package vars
    20  
    21  import (
    22  	"context"
    23  	"fmt"
    24  	"regexp"
    25  	"sync"
    26  
    27  	"go.chromium.org/luci/common/errors"
    28  )
    29  
    30  // Vars is the default set of placeholder vars used by the process.
    31  //
    32  // Individual packages may register vars here during init() time.
    33  var Vars VarSet
    34  
    35  // VarSet holds a registry ${var} -> callback that extracts it from a context.
    36  //
    37  // It is used by the config client to render config validation patterns like
    38  // "services/${appid}". By using placeholder vars it is possible to register
    39  // validation rules during process startup time before values of all vars
    40  // are actually known.
    41  type VarSet struct {
    42  	l sync.RWMutex
    43  	v map[string]func(context.Context) (string, error)
    44  }
    45  
    46  // Register registers a variable that can be used in templates as `${name}`.
    47  //
    48  // Such placeholder variable is rendered into an actual value via the given
    49  // callback in RenderTemplate.
    50  //
    51  // The primary use case for this mechanism is to allow to register config
    52  // validation rule patterns that depend on not-yet known values during init()
    53  // time.
    54  //
    55  // Panics if such variable is already registered.
    56  func (vs *VarSet) Register(name string, value func(context.Context) (string, error)) {
    57  	vs.l.Lock()
    58  	defer vs.l.Unlock()
    59  	if vs.v == nil {
    60  		vs.v = make(map[string]func(context.Context) (string, error), 1)
    61  	}
    62  	if vs.v[name] != nil {
    63  		panic(fmt.Sprintf("variable %q is already registered", name))
    64  	}
    65  	vs.v[name] = value
    66  }
    67  
    68  var placeholderRe = regexp.MustCompile(`\${[^}]*}`)
    69  
    70  // RenderTemplate replaces all `${var}` references in the string with their
    71  // values by calling registered callbacks.
    72  func (vs *VarSet) RenderTemplate(ctx context.Context, templ string) (string, error) {
    73  	vs.l.RLock()
    74  	defer vs.l.RUnlock()
    75  
    76  	var errs errors.MultiError
    77  
    78  	out := placeholderRe.ReplaceAllStringFunc(templ, func(match string) string {
    79  		name := match[2 : len(match)-1] // strip ${...}
    80  
    81  		var val string
    82  		var err error
    83  
    84  		if cb := vs.v[name]; cb != nil {
    85  			val, err = cb(ctx)
    86  		} else {
    87  			err = fmt.Errorf("no placeholder named %q is registered", name)
    88  		}
    89  
    90  		if err != nil {
    91  			errs = append(errs, err)
    92  		}
    93  		return val
    94  	})
    95  
    96  	if len(errs) != 0 {
    97  		return "", errs
    98  	}
    99  	return out, nil
   100  }