blob: 464f5d1ffbe5d80baf992abe3efd7248e3db5d2f [file] [log] [blame]
package config_vars
/*
Package config_vars provides helpers for configuration templates.
*/
import (
"bytes"
"context"
"encoding/json"
"html/template"
"strings"
"sync"
"go.skia.org/infra/go/chrome_branch"
"go.skia.org/infra/go/skerr"
)
var (
// These templates should not be used.
bannedTmpls = []string{
"Chromium.Master.Number",
}
)
// Vars represents current values of variables which may be used in templates.
type Vars struct {
Branches *Branches `json:"branches"`
}
// Validate returns an error if Vars is not valid.
func (v *Vars) Validate() error {
if v.Branches == nil {
return skerr.Fmt("Branches is required.")
}
if err := v.Branches.Validate(); err != nil {
return skerr.Wrap(err)
}
return nil
}
// Copy returns a deep copy of the Vars.
func (v *Vars) Copy() *Vars {
if v == nil {
return nil
}
return &Vars{
Branches: v.Branches.Copy(),
}
}
// FakeVars returns an instance of Vars with arbitrary contents which may be
// used during validation and testing.
func FakeVars() *Vars {
activeMilestones := []*chrome_branch.Branch{
{
Milestone: 82,
Number: 0,
Ref: chrome_branch.RefMain,
V8Branch: "8.2",
},
{
Milestone: 81,
Number: 4044,
Ref: chrome_branch.ReleaseBranchRef(4044),
V8Branch: "8.1",
},
{
Milestone: 80,
Number: 3987,
Ref: chrome_branch.ReleaseBranchRef(4044),
V8Branch: "8.0",
},
}
return &Vars{
Branches: &Branches{
ActiveMilestones: activeMilestones,
Chromium: &chrome_branch.Branches{
Main: activeMilestones[0],
Beta: activeMilestones[1],
Stable: activeMilestones[2],
},
},
}
}
// Branches represents named branches in git repositories.
type Branches struct {
// ActiveMilestones are the active Chromium milestones.
ActiveMilestones []*chrome_branch.Branch `json:"active_milestones"`
// Chromium release branches.
Chromium *chrome_branch.Branches `json:"chromium"`
}
// Validate returns an error if Branches is not valid.
func (b *Branches) Validate() error {
if b.ActiveMilestones == nil || len(b.ActiveMilestones) == 0 {
return skerr.Fmt("ActiveMilestones is required.")
}
for _, m := range b.ActiveMilestones {
if err := m.Validate(); err != nil {
return skerr.Wrap(err)
}
}
if b.Chromium == nil {
return skerr.Fmt("Chromium is required.")
}
if err := b.Chromium.Validate(); err != nil {
return skerr.Wrap(err)
}
return nil
}
// Copy returns a deep copy of the Branches.
func (b *Branches) Copy() *Branches {
if b == nil {
return nil
}
activeMilestones := make([]*chrome_branch.Branch, 0, len(b.ActiveMilestones))
for _, m := range b.ActiveMilestones {
activeMilestones = append(activeMilestones, m.Copy())
}
var chromium *chrome_branch.Branches
if b.Chromium != nil {
chromium = b.Chromium.Copy()
}
return &Branches{
ActiveMilestones: activeMilestones,
Chromium: chromium,
}
}
// Template is a text template which uses the contents of Vars. It is
// technically thread-safe, but users should take care to cache the value of
// Template.String() rather than calling it repeatedly, unless Update() is
// only ever called within the same thread as String().
type Template struct {
mtx sync.RWMutex
raw string
tmpl *template.Template
value string
}
// NewTemplate returns a Template instance.
func NewTemplate(rawTmpl string) (*Template, error) {
rv := &Template{}
if err := rv.init(rawTmpl); err != nil {
return nil, skerr.Wrap(err)
}
return rv, nil
}
// init initializes the template.
func (t *Template) init(rawTmpl string) error {
for _, banned := range bannedTmpls {
if strings.Contains(rawTmpl, banned) {
return skerr.Fmt("Templates should not use %q", banned)
}
}
tmpl, err := template.New("").Parse(rawTmpl)
if err != nil {
return skerr.Wrap(err)
}
tmpl.Option("missingkey=error")
t.raw = rawTmpl
t.tmpl = tmpl
return skerr.Wrap(t.validate())
}
// UnmarshalJSON implements json.Unmarshaler.
func (t *Template) UnmarshalJSON(b []byte) error {
t.mtx.Lock()
defer t.mtx.Unlock()
var raw string
if err := json.Unmarshal(b, &raw); err != nil {
return skerr.Wrap(err)
}
return skerr.Wrap(t.init(raw))
}
// MarshalJSON implements json.Marshaler.
func (t *Template) MarshalJSON() ([]byte, error) {
t.mtx.RLock()
defer t.mtx.RUnlock()
return json.Marshal(t.raw)
}
// Validate returns an error if the Template is not valid.
func (t *Template) Validate() error {
t.mtx.RLock()
defer t.mtx.RUnlock()
return skerr.Wrap(t.validate())
}
// validate returns an error if the Template is not valid. Assumes the caller
// holds t.mtx.
func (t *Template) validate() error {
if t == nil || t.raw == "" || t.tmpl == nil {
return skerr.Fmt("Template is missing.")
}
// Create fake Vars to test the template.
v := FakeVars()
// Self-check, in case any of the members of Vars change.
if err := v.Validate(); err != nil {
return skerr.Wrap(err)
}
var buf bytes.Buffer
if err := t.tmpl.Execute(&buf, v); err != nil {
return skerr.Wrap(err)
}
return nil
}
// Update executes the Template using the given Vars and updates its value.
func (t *Template) Update(v *Vars) error {
t.mtx.Lock()
defer t.mtx.Unlock()
var buf bytes.Buffer
if err := t.tmpl.Execute(&buf, v); err != nil {
return skerr.Wrap(err)
}
t.value = buf.String()
return nil
}
// String returns the current value of the Template.
func (t *Template) String() string {
t.mtx.RLock()
defer t.mtx.RUnlock()
return t.value
}
// Equal returns true iff the given Template is equivalent to this one. This
// allows us to test equality using deepequal.DeepEqual, despite the fact that
// text.Templates are never DeepEqual.
func (t *Template) Equal(t2 *Template) bool {
t.mtx.RLock()
defer t.mtx.RUnlock()
t2.mtx.RLock()
defer t2.mtx.RUnlock()
if t.raw != t2.raw {
return false
}
if t.value != t2.value {
return false
}
return true
}
// RawTemplate returns the raw string value of the Template.
func (t *Template) RawTemplate() string {
return t.raw
}
// Registry manages a set of Templates, updating their values when Update is
// called.
type Registry struct {
cbc chrome_branch.Client
mtx sync.Mutex
templates []*Template
vars *Vars
}
// NewRegistry returns a Registry instance. Implicitly runs Update().
func NewRegistry(ctx context.Context, cbc chrome_branch.Client) (*Registry, error) {
r := &Registry{
cbc: cbc,
}
if err := r.Update(ctx); err != nil {
return nil, err
}
return r, nil
}
// Register the given Template. Implicitly Updates its value using the most
// recent Vars.
func (r *Registry) Register(t *Template) error {
r.mtx.Lock()
defer r.mtx.Unlock()
if err := t.Update(r.vars); err != nil {
return err
}
r.templates = append(r.templates, t)
return nil
}
// updateFrom updates all of the Templates managed by this Registry using the
// given Vars.
func (r *Registry) updateFrom(v *Vars) error {
r.mtx.Lock()
defer r.mtx.Unlock()
for _, tmpl := range r.templates {
if err := tmpl.Update(v); err != nil {
return skerr.Wrap(err)
}
}
r.vars = v
return nil
}
// Update all of the Templates managed by this Registry by loading new values.
func (r *Registry) Update(ctx context.Context) error {
chromeBranches, activeMilestones, err := r.cbc.Get(ctx)
if err != nil {
return skerr.Wrap(err)
}
vars := &Vars{
Branches: &Branches{
ActiveMilestones: activeMilestones,
Chromium: chromeBranches,
},
}
return skerr.Wrap(r.updateFrom(vars))
}
// Vars returns a copy of the current value of the stored Vars.
func (r *Registry) Vars() *Vars {
r.mtx.Lock()
defer r.mtx.Unlock()
return r.vars.Copy()
}