blob: 2846611e711ffc44fff8251831cabef70b19b667 [file]
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package config
import (
"fmt"
"strings"
"go.chromium.org/luci/common/errors"
tricium "infra/tricium/api/v1"
)
// Validate validates the provided project config using the provided service
// config.
//
// They are validated together because a project config may contain a selection
// which is valid or invalid depending on the service config.
//
// Returns an error if invalid, or nil if valid.
func Validate(sc *tricium.ServiceConfig, pc *tricium.ProjectConfig) error {
if sc.BuildbucketServerHost == "" {
return errors.Reason("missing Buildbucket server host in service config").Err()
}
functions, err := mergeSelectedFunctions(sc, pc)
if err != nil {
return errors.Annotate(err, "failed to merge functions").Err()
}
for _, s := range pc.Selections {
name := s.Function
f := functions[name]
if err := tricium.ValidateFunction(f, sc); err != nil {
return errors.Annotate(err, "function is not valid").Err()
}
if strings.Contains(s.Platform.String(), "_") {
// Underscore is used as a separator character in worker names.
return errors.Reason("platform %q should not contain underscore character", s.Platform).Err()
}
if !tricium.SupportsPlatform(f, s.Platform) {
return errors.Reason("no support for platform %s by function %s", s.Platform, name).Err()
}
}
return nil
}
// Merges all function definitions for selections in the project config.
func mergeSelectedFunctions(sc *tricium.ServiceConfig, pc *tricium.ProjectConfig) (map[string]*tricium.Function, error) {
functions := map[string]*tricium.Function{}
for _, s := range pc.Selections {
// Get merged function definition.
name := s.Function
if _, ok := functions[name]; !ok {
sf := tricium.LookupFunction(sc.Functions, name)
pf := tricium.LookupFunction(pc.Functions, name)
f, err := mergeFunction(name, sc, sf, pf)
if err != nil {
return nil, err
}
functions[name] = f
}
}
return functions, nil
}
// mergeFunction merges the provided service and project function definitions.
//
// In merging, the following override rules are applied:
// - existence of project path_filters fully replaces any service path_filters.
// - project impl for platform fully replaces service impl for the same platform.
// - project owner and component from project overrides service if set.
//
// While merging, mergeFunctions also validates the functions.
//
// Possible errors include:
// - change of data dependency in service config is not allowed.
// - project and service config functions must have the same type.
func mergeFunction(function string, sc *tricium.ServiceConfig, sf, pf *tricium.Function) (*tricium.Function, error) {
if sf == nil && pf == nil {
return nil, errors.Reason("unknown function %s", function).Err()
}
res := &tricium.Function{Name: function}
if sf != nil {
if err := tricium.ValidateFunction(sf, sc); err != nil {
return nil, errors.Reason("invalid service function config for %s: %v", function, err).Err()
}
if sf.GetNeeds() == tricium.Data_NONE || sf.GetProvides() == tricium.Data_NONE {
return nil, errors.Reason("service function config must have data dependencies, function: %s", function).Err()
}
res.Type = sf.Type
res.Needs = sf.Needs
res.Provides = sf.Provides
res.PathFilters = sf.PathFilters
res.Owner = sf.Owner
res.MonorailComponent = sf.MonorailComponent
res.Impls = sf.Impls
}
if pf != nil {
if sf != nil &&
(sf.Type != pf.Type) {
return nil, fmt.Errorf("cannot merge functions of different type, name: %s, service type: %s, project type: %s",
pf.Name, sf.Type, pf.Type)
}
res.Type = pf.Type
if sf != nil &&
(pf.GetNeeds() != tricium.Data_NONE && pf.GetNeeds() != sf.GetNeeds() ||
pf.GetProvides() != tricium.Data_NONE && pf.GetProvides() != sf.GetProvides()) {
return nil, errors.Reason("change of service function data dependencies not allowed, function: %s", function).Err()
}
if sf == nil {
if pf.GetNeeds() == tricium.Data_NONE || pf.GetProvides() == tricium.Data_NONE {
return nil, errors.Reason("project function config is missing data dependencies, function: %s", function).Err()
}
res.Needs = pf.Needs
res.Provides = pf.Provides
}
// Add service deps to project entry for validation check.
// These deps are used to check validity of impls and when
// there are service deps they are inherited by the project
// entry.
if sf != nil {
pf.Needs = sf.Needs
pf.Provides = sf.Provides
}
if err := tricium.ValidateFunction(pf, sc); err != nil {
return nil, errors.Reason("invalid project function config for %s: %v", function, err).Err()
}
if pf.GetPathFilters() != nil {
res.PathFilters = pf.PathFilters
}
if pf.GetOwner() != "" {
res.Owner = pf.Owner
}
if pf.GetMonorailComponent() != "" {
res.MonorailComponent = pf.MonorailComponent
}
if sf != nil {
res.Impls = mergeImpls(sf.Impls, pf.Impls)
} else {
res.Impls = pf.Impls
}
}
return res, nil
}
// mergeImpls merges the service function implementations with the project
// function implementations.
//
// All provided impl entries are assumed to be valid. The project
// implementations can override the service implementations for the same
// platform.
func mergeImpls(sci []*tricium.Impl, pci []*tricium.Impl) []*tricium.Impl {
impls := map[tricium.Platform_Name]*tricium.Impl{}
for _, i := range sci {
impls[i.ProvidesForPlatform] = i
}
for _, i := range pci {
impls[i.ProvidesForPlatform] = i
}
res := []*tricium.Impl{}
for _, v := range impls {
res = append(res, v)
}
return res
}