func ReadGCS(ctx context.Context, opener gcs.Opener, path gcs.Path) (*configpb.Configuration, error) {
	r, _, err := opener.Open(ctx, path)
	if err != nil {
		return nil, fmt.Errorf("failed to open config: %v", err)
	}
	return Unmarshal(r)
}
ReadGCS opens the config at path and unmarshals it into a Configuration proto.
package config
import (
	"context"
	"errors"
	"fmt"
	"io"
	"io/ioutil"
	"os"
	"regexp"
	"strings"
	"unicode/utf8"
	"cloud.google.com/go/storage"
	"github.com/golang/protobuf/proto"
	configpb "github.com/GoogleCloudPlatform/testgrid/pb/config"
	"github.com/GoogleCloudPlatform/testgrid/util/gcs"
	multierror "github.com/hashicorp/go-multierror"
)
type MissingFieldError struct {
	Field string
}
func (e MissingFieldError) Error() string {
	return fmt.Sprintf("field missing or unset: %s", e.Field)
}
type DuplicateNameError struct {
	Name   string
	Entity string
}
func (e DuplicateNameError) Error() string {
	return fmt.Sprintf("found duplicate name after normalizing: (%s) %s", e.Entity, e.Name)
}
type MissingEntityError struct {
	Name   string
	Entity string
}
func (e MissingEntityError) Error() string {
	return fmt.Sprintf("could not find the referenced (%s) %s", e.Entity, e.Name)
}
type ValidationError struct {
	Name    string
	Entity  string
	Message string
}
func (e ValidationError) Error() string {
	return fmt.Sprintf("configuration error for (%s) %s: %s", e.Entity, e.Name, e.Message)
}
func Normalize(s string) string {
	regex := regexp.MustCompile("[^a-zA-Z0-9]+")
	s = regex.ReplaceAllString(s, "")
	s = strings.ToLower(s)
	return s
}
const minNameLength = 3
const maxNameLength = 2048
func validateUnique(items []string, entity string) error {
	var mErr error
	set := map[string]bool{}
	for _, item := range items {
		s := Normalize(item)
		_, ok := set[s]
		if ok {
			mErr = multierror.Append(mErr, DuplicateNameError{s, entity})
		} else {
			set[s] = true
		}
	}
	return mErr
}
func validateAllUnique(c *configpb.Configuration) error {
	var mErr error
	if c == nil {
		return multierror.Append(mErr, errors.New("got an empty config.Configuration"))
	}
	var tgNames []string
	for _, tg := range c.GetTestGroups() {
		if err := validateName(tg.GetName()); err != nil {
			mErr = multierror.Append(mErr, &ValidationError{tg.GetName(), "TestGroup", err.Error()})
		}
		tgNames = append(tgNames, tg.GetName())
	}
	if err := validateUnique(tgNames, "TestGroup"); err != nil {
		mErr = multierror.Append(mErr, err)
	}
	var dashNames []string
	for _, dash := range c.GetDashboards() {
		if err := validateName(dash.Name); err != nil {
			mErr = multierror.Append(mErr, &ValidationError{dash.GetName(), "Dashboard", err.Error()})
		}
		dashNames = append(dashNames, dash.Name)
		var tabNames []string
		for _, tab := range dash.GetDashboardTab() {
			if err := validateName(tab.Name); err != nil {
				mErr = multierror.Append(mErr, &ValidationError{tab.Name, "DashboardTab", err.Error()})
			}
			tabNames = append(tabNames, tab.Name)
		}
		if err := validateUnique(tabNames, "DashboardTab"); err != nil {
			mErr = multierror.Append(mErr, err)
		}
	}
	if err := validateUnique(dashNames, "Dashboard"); err != nil {
		mErr = multierror.Append(mErr, err)
	}
	var dgNames []string
	for _, dg := range c.GetDashboardGroups() {
		if err := validateName(dg.Name); err != nil {
			mErr = multierror.Append(mErr, &ValidationError{dg.Name, "DashboardGroup", err.Error()})
		}
		dgNames = append(dgNames, dg.Name)
	}
	if err := validateUnique(dgNames, "DashboardGroup"); err != nil {
		mErr = multierror.Append(mErr, err)
	}
	if err := validateUnique(append(dashNames, dgNames...), "Dashboard/DashboardGroup"); err != nil {
		mErr = multierror.Append(mErr, err)
	}
	return mErr
}
func validateReferencesExist(c *configpb.Configuration) error {
	var mErr error
	if c == nil {
		return multierror.Append(mErr, errors.New("got an empty config.Configuration"))
	}
	tgNames := map[string]bool{}
	for _, tg := range c.GetTestGroups() {
		tgNames[tg.GetName()] = true
	}
	tgInTabs := map[string]bool{}
	for _, dash := range c.GetDashboards() {
		for _, tab := range dash.DashboardTab {
			tabTg := tab.TestGroupName
			tgInTabs[tabTg] = true
			if _, ok := tgNames[tabTg]; !ok {
				mErr = multierror.Append(mErr, MissingEntityError{tabTg, "TestGroup"})
			}
		}
	}
	for tgName := range tgNames {
		if _, ok := tgInTabs[tgName]; !ok {
			mErr = multierror.Append(mErr, ValidationError{tgName, "TestGroup", "Each Test Group must be referenced by at least 1 Dashboard Tab."})
		}
	}
	dashNames := map[string]bool{}
	for _, dash := range c.GetDashboards() {
		dashNames[dash.Name] = true
	}
	dashToDg := map[string]bool{}
	for _, dg := range c.GetDashboardGroups() {
		for _, name := range dg.DashboardNames {
			dgDash := name
			if _, ok := dashNames[dgDash]; !ok {
				mErr = multierror.Append(mErr, MissingEntityError{dgDash, "Dashboard"})
			} else if _, ok = dashToDg[dgDash]; ok {
				mErr = multierror.Append(mErr, ValidationError{dgDash, "Dashboard", "A Dashboard cannot be in more than 1 Dashboard Group."})
			} else {
				dashToDg[dgDash] = true
			}
		}
	}
	return mErr
}
func validateName(s string) error {
	name := Normalize(s)
	if len(name) < minNameLength {
		return fmt.Errorf("names must contain at least %d alphanumeric characters", minNameLength)
	}
	if len(name) > maxNameLength {
		return fmt.Errorf("names should not contain more than %d alphanumeric characters", maxNameLength)
	}
	invalidPrefixes := []string{"dashboard", "alerter", "summary", "bugs"}
	for _, prefix := range invalidPrefixes {
		if strings.HasPrefix(name, prefix) {
			return fmt.Errorf("normalized name can't be prefixed with any of %v", invalidPrefixes)
		}
	}
	return nil
}
func validateEmails(addresses string) error {
	regex := regexp.MustCompile("^[^@]+@[^@]+$")
	invalid := []string{}
	for _, address := range strings.Split(addresses, ",") {
		match := regex.Match([]byte(address))
		if !match {
			invalid = append(invalid, address)
		}
	}
	if len(invalid) > 0 {
		return fmt.Errorf("bad emails %v specified in '%s'; an email address should have exactly one at (@) symbol)", invalid, addresses)
	}
	return nil
}
func validateTestGroup(tg *configpb.TestGroup) error {
	var mErr error
	if tg == nil {
		return multierror.Append(mErr, errors.New("got an empty TestGroup"))
	}
	if tg.GetGcsPrefix() == "" && tg.GetResultSource() == nil {
		mErr = multierror.Append(mErr, errors.New("require one of gcs_prefix or result_source"))
	}
	if tg.GetDaysOfResults() <= 0 {
		mErr = multierror.Append(mErr, errors.New("days_of_results should be positive"))
	}
	if tg.GetNumColumnsRecent() <= 0 {
		mErr = multierror.Append(mErr, errors.New("num_columns_recent should be positive"))
	}
	if _, err := regexp.Compile(tg.GetTestMethodMatchRegex()); err != nil {
		mErr = multierror.Append(mErr, fmt.Errorf("test_method_match_regex doesn't compile: %v", err))
	}
	if tg.GetAlertMailToAddresses() != "" {
		if err := validateEmails(tg.GetAlertMailToAddresses()); err != nil {
			mErr = multierror.Append(mErr, err)
		}
	}
	metadataOpts := tg.GetTestMetadataOptions()
	for _, opt := range metadataOpts {
		if opt.GetBugComponent() <= 0 {
			mErr = multierror.Append(mErr, errors.New("bug_component is required"))
		}
		if opt.GetMessageRegex() == "" && opt.GetTestNameRegex() == "" {
			mErr = multierror.Append(mErr, errors.New("at least one of message_regex or test_name_regex must be specified"))
		}
		if _, err := regexp.Compile(opt.GetMessageRegex()); err != nil {
			mErr = multierror.Append(mErr, fmt.Errorf("message_regex doesn't compile: %v", err))
		}
		if _, err := regexp.Compile(opt.GetTestNameRegex()); err != nil {
			mErr = multierror.Append(mErr, fmt.Errorf("test_name_regex doesn't compile: %v", err))
		}
	}
	for _, notification := range tg.GetNotifications() {
		if notification.GetSummary() == "" {
			mErr = multierror.Append(mErr, errors.New("summary is required"))
		}
	}
	annotations := tg.GetTestAnnotations()
	for _, annotation := range annotations {
		if annotation.GetPropertyName() == "" {
			mErr = multierror.Append(mErr, errors.New("property_name is required"))
		}
		if annotation.GetShortText() == "" || utf8.RuneCountInString(annotation.GetShortText()) > 5 {
			mErr = multierror.Append(mErr, errors.New("short_text must be 1-5 characters long"))
		}
	}
	fallbackConfigSettingSet := tg.GetFallbackGrouping() == configpb.TestGroup_FALLBACK_GROUPING_CONFIGURATION_VALUE
	fallbackConfigValueSet := tg.GetFallbackGroupingConfigurationValue() != ""
	if fallbackConfigSettingSet != fallbackConfigValueSet {
		mErr = multierror.Append(
			mErr,
			errors.New("fallback_grouping_configuration_value and fallback_grouping = FALLBACK_GROUPING_CONFIGURATION_VALUE require each other"),
		)
	}
	for idx, header := range tg.GetColumnHeader() {
		if cv, p, l := header.ConfigurationValue, header.Property, header.Label; cv == "" && p == "" && l == "" {
			mErr = multierror.Append(mErr, &ValidationError{tg.GetName(), "TestGroup", fmt.Sprintf("Column Header %d is empty", idx)})
		} else if cv != "" && (p != "" || l != "") || p != "" && (cv != "" || l != "") {
			mErr = multierror.Append(
				mErr,
				fmt.Errorf("Column Header %d must only set one value, got configuration_value: %q, property: %q, label: %q", idx, cv, p, l),
			)
		}
	}
	if tg.GetTestNameConfig() != nil {
		nameFormat := tg.GetTestNameConfig().GetNameFormat()
		nameElements := tg.GetTestNameConfig().GetNameElements()
		if len(nameElements) == 0 {
			mErr = multierror.Append(mErr, errors.New("TestNameConfig.NameElements must be specified"))
		}
		if nameFormat == "" {
			mErr = multierror.Append(mErr, errors.New("TestNameConfig.NameFormat must be specified"))
		} else {
			if got, want := len(nameElements), strings.Count(nameFormat, "%"); got != want {
				mErr = multierror.Append(
					mErr,
					fmt.Errorf("TestNameConfig has %d elements, format %s wants %d", got, nameFormat, want),
				)
			}
			elements := make([]interface{}, 0)
			for range nameElements {
				elements = append(elements, "")
			}
			s := fmt.Sprintf(nameFormat, elements...)
			if strings.Contains(s, "%!") {
				return fmt.Errorf("number of format strings and name_elements must match; got %s (%d)", s, len(elements))
			}
		}
	}
	return mErr
}
func validateDashboardTab(dt *configpb.DashboardTab) error {
	var mErr error
	if dt == nil {
		return multierror.Append(mErr, errors.New("got an empty DashboardTab"))
	}
	if dt.GetTestGroupName() == "" {
		mErr = multierror.Append(mErr, errors.New("test_group_name can't be empty"))
	}
	if dt.GetName() == "Summary" {
		mErr = multierror.Append(mErr, errors.New("tab can't be named 'Summary'"))
	}
	if dt.GetTabularNamesRegex() != "" {
		regex, err := regexp.Compile(dt.GetTabularNamesRegex())
		if err != nil {
			mErr = multierror.Append(
				mErr,
				fmt.Errorf("invalid regex %s: %v", dt.GetTabularNamesRegex(), err))
		} else {
			var names []string
			for _, subexpName := range regex.SubexpNames() {
				if subexpName != "" {
					names = append(names, subexpName)
				}
			}
			if regex.NumSubexp() != len(names) {
				mErr = multierror.Append(mErr, fmt.Errorf("all tabular_name_regex capture groups must be named"))
			}
			if len(names) < 1 {
				mErr = multierror.Append(mErr, errors.New("tabular_name_regex requires at least one capture group"))
			}
		}
	}
	if dt.GetAlertOptions().GetAlertMailToAddresses() != "" {
		if err := validateEmails(dt.GetAlertOptions().GetAlertMailToAddresses()); err != nil {
			mErr = multierror.Append(mErr, err)
		}
	}
	return mErr
}
func validateEntityConfigs(c *configpb.Configuration) error {
	var mErr error
	if c == nil {
		return multierror.Append(mErr, errors.New("got an empty config.Configuration"))
	}
	for _, tg := range c.GetTestGroups() {
		if err := validateTestGroup(tg); err != nil {
			mErr = multierror.Append(mErr, &ValidationError{tg.GetName(), "TestGroup", err.Error()})
		}
	}
	for _, d := range c.GetDashboards() {
		for _, dt := range d.DashboardTab {
			if err := validateDashboardTab(dt); err != nil {
				mErr = multierror.Append(mErr, &ValidationError{dt.GetName(), "DashboardTab", err.Error()})
			}
		}
	}
	return mErr
}
func Validate(c *configpb.Configuration) error {
	var mErr error
	if c == nil {
		return multierror.Append(mErr, errors.New("got an empty config.Configuration"))
	}
	if len(c.GetTestGroups()) == 0 {
		return multierror.Append(mErr, MissingFieldError{"TestGroups"})
	}
	if len(c.GetDashboards()) == 0 {
		return multierror.Append(mErr, MissingFieldError{"Dashboards"})
	}
	for _, dashboard := range c.GetDashboards() {
		if len(dashboard.DashboardTab) == 0 {
			mErr = multierror.Append(mErr, ValidationError{dashboard.Name, "Dashboard", "contains no tabs"})
		}
	}
	if err := validateAllUnique(c); err != nil {
		mErr = multierror.Append(mErr, err)
	}
	if err := validateReferencesExist(c); err != nil {
		mErr = multierror.Append(mErr, err)
	}
	if err := validateEntityConfigs(c); err != nil {
		mErr = multierror.Append(mErr, err)
	}
	return mErr
}
func Unmarshal(r io.Reader) (*configpb.Configuration, error) {
	buf, err := ioutil.ReadAll(r)
	if err != nil {
		return nil, fmt.Errorf("failed to read config: %v", err)
	}
	var cfg configpb.Configuration
	if err = proto.Unmarshal(buf, &cfg); err != nil {
		return nil, fmt.Errorf("failed to parse: %v", err)
	}
	return &cfg, nil
}
func MarshalText(c *configpb.Configuration, w io.Writer) error {
	if c == nil {
		return errors.New("got an empty config.Configuration")
	}
	if err := Validate(c); err != nil {
		return err
	}
	return proto.MarshalText(w, c)
}
func MarshalBytes(c *configpb.Configuration) ([]byte, error) {
	if c == nil {
		return nil, errors.New("got an empty config.Configuration")
	}
	if err := Validate(c); err != nil {
		return nil, err
	}
	return proto.Marshal(c)
}