blob: 4a92cb643b9bf749c6435f48db72c1981b22b1e8 [file] [log] [blame]
// Copyright 2023 The LUCI Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package cfg
import (
"context"
"fmt"
"strings"
"testing"
"go.chromium.org/luci/common/errors"
"go.chromium.org/luci/common/testing/ftt"
"go.chromium.org/luci/common/testing/truth/assert"
"go.chromium.org/luci/common/testing/truth/should"
"go.chromium.org/luci/config/validation"
configpb "go.chromium.org/luci/swarming/proto/config"
"go.chromium.org/luci/swarming/server/validate"
)
var goodTaskTemplate = &configpb.TaskTemplate{
Cache: []*configpb.TaskTemplate_CacheEntry{
{
Name: "c1",
Path: "a/b",
},
},
CipdPackage: []*configpb.TaskTemplate_CipdPackage{
{
Pkg: "a",
Version: "latest",
Path: "c/d",
},
},
Env: []*configpb.TaskTemplate_Env{
{
Var: "ev",
Value: "b",
Prefix: []string{
"e/f",
},
Soft: true,
},
},
}
var goodPoolsCfg = &configpb.PoolsCfg{
TaskTemplate: []*configpb.TaskTemplate{
{
Name: "t1",
Cache: goodTaskTemplate.Cache,
CipdPackage: goodTaskTemplate.CipdPackage,
Env: goodTaskTemplate.Env,
},
},
TaskTemplateDeployment: []*configpb.TaskTemplateDeployment{
{
Name: "d1",
Prod: &configpb.TaskTemplate{
Include: []string{
"t1",
},
},
Canary: goodTaskTemplate,
CanaryChance: 1000,
},
},
Pool: []*configpb.Pool{
{
Name: []string{"a"},
Realm: "test:1",
DefaultTaskRealm: "test:default",
TaskDeploymentScheme: &configpb.Pool_TaskTemplateDeployment{
TaskTemplateDeployment: "d1",
},
},
{
Name: []string{"b", "c"},
Realm: "test:2",
TaskDeploymentScheme: &configpb.Pool_TaskTemplateDeploymentInline{
TaskTemplateDeploymentInline: &configpb.TaskTemplateDeployment{
Prod: goodTaskTemplate,
Canary: goodTaskTemplate,
CanaryChance: 100,
},
},
RbeMigration: &configpb.Pool_RBEMigration{
RbeInstance: "some-instance",
RbeModePercent: 66,
BotModeAllocation: []*configpb.Pool_RBEMigration_BotModeAllocation{
{Mode: configpb.Pool_RBEMigration_BotModeAllocation_SWARMING, Percent: 5},
{Mode: configpb.Pool_RBEMigration_BotModeAllocation_HYBRID, Percent: 10},
{Mode: configpb.Pool_RBEMigration_BotModeAllocation_RBE, Percent: 85},
},
},
},
},
DefaultExternalServices: &configpb.ExternalServices{
Cipd: &configpb.ExternalServices_CIPD{
Server: "https://cipd.example.com",
ClientPackage: &configpb.CipdPackage{
PackageName: "client/pkg",
Version: "latest",
},
},
},
}
func TestNewPoolsConfig(t *testing.T) {
t.Parallel()
ftt.Run("Works", t, func(t *ftt.Test) {
pools, err := newPoolsConfig(goodPoolsCfg)
assert.Loosely(t, err, should.BeNil)
assert.Loosely(t, pools, should.HaveLength(3))
assert.Loosely(t, pools["a"].Realm, should.Equal("test:1"))
assert.Loosely(t, pools["b"].Realm, should.Equal("test:2"))
assert.Loosely(t, pools["c"].Realm, should.Equal("test:2"))
assert.Loosely(t, pools["a"].DefaultTaskRealm, should.Equal("test:default"))
assert.Loosely(t, pools["b"].DefaultTaskRealm, should.BeEmpty)
assert.Loosely(t, pools["c"].DefaultTaskRealm, should.BeEmpty)
expectedInline := &configpb.TaskTemplateDeployment{
Prod: goodTaskTemplate,
Canary: goodTaskTemplate,
CanaryChance: 100,
}
assert.Loosely(t, pools["b"].Deployment, should.Match(expectedInline))
expectedShared := &configpb.TaskTemplateDeployment{
Name: "d1",
Prod: goodTaskTemplate,
Canary: goodTaskTemplate,
CanaryChance: 1000,
}
assert.Loosely(t, pools["a"].Deployment, should.Match(expectedShared))
assert.Loosely(t, pools["b"].RBEInstance, should.Equal("some-instance"))
assert.Loosely(t, pools["b"].RBEModePercent, should.Equal(66))
})
}
func TestPoolsValidation(t *testing.T) {
t.Parallel()
call := func(cfg *configpb.PoolsCfg) []string {
ctx := validation.Context{Context: context.Background()}
ctx.SetFile("pools.cfg")
validatePoolsCfg(&ctx, cfg)
if err := ctx.Finalize(); err != nil {
var verr *validation.Error
errors.As(err, &verr)
out := make([]string, len(verr.Errors))
for i, err := range verr.Errors {
out[i] = err.Error()
}
return out
}
return nil
}
ftt.Run("Empty", t, func(t *ftt.Test) {
assert.Loosely(t, call(&configpb.PoolsCfg{}), should.BeNil)
})
ftt.Run("Good", t, func(t *ftt.Test) {
assert.Loosely(t, call(goodPoolsCfg), should.BeNil)
})
ftt.Run("Errors", t, func(t *ftt.Test) {
t.Run("default_cipd", func(t *ftt.Test) {
testCases := []struct {
name string
cfg *configpb.PoolsCfg
err []string
}{
{
name: "no_server",
cfg: &configpb.PoolsCfg{
DefaultExternalServices: &configpb.ExternalServices{
Cipd: &configpb.ExternalServices_CIPD{
ClientPackage: &configpb.CipdPackage{
PackageName: "client/pkg",
Version: "latest",
},
},
},
},
err: []string{`(default_cipd / server): required`},
},
{
name: "no_client_package",
cfg: &configpb.PoolsCfg{
DefaultExternalServices: &configpb.ExternalServices{
Cipd: &configpb.ExternalServices_CIPD{
Server: "https://cipd.example.com",
},
},
},
err: []string{`(default_cipd / client_package): required`},
},
{
name: "no_package_name",
cfg: &configpb.PoolsCfg{
DefaultExternalServices: &configpb.ExternalServices{
Cipd: &configpb.ExternalServices_CIPD{
Server: "https://cipd.example.com",
ClientPackage: &configpb.CipdPackage{
Version: "latest",
},
},
},
},
err: []string{`(default_cipd / client_package / name): required`},
},
{
name: "no_package_version",
cfg: &configpb.PoolsCfg{
DefaultExternalServices: &configpb.ExternalServices{
Cipd: &configpb.ExternalServices_CIPD{
Server: "https://cipd.example.com",
ClientPackage: &configpb.CipdPackage{
PackageName: "client/pkg",
},
},
},
},
err: []string{`(default_cipd / client_package / version): required`},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *ftt.Test) {
for i := range tc.err {
tc.err[i] = `in "pools.cfg" ` + tc.err[i]
}
assert.Loosely(t, call(tc.cfg), should.Match(tc.err))
})
}
})
t.Run("pool", func(t *ftt.Test) {
onePool := func(p *configpb.Pool) *configpb.PoolsCfg {
return &configpb.PoolsCfg{
Pool: []*configpb.Pool{p},
DefaultExternalServices: &configpb.ExternalServices{
Cipd: &configpb.ExternalServices_CIPD{
Server: "https://cipd.example.com",
ClientPackage: &configpb.CipdPackage{
PackageName: "client/pkg",
Version: "latest",
},
},
},
}
}
testCases := []struct {
cfg *configpb.PoolsCfg
err string
}{
// pool
{
cfg: onePool(&configpb.Pool{
Name: []string{"a"},
Realm: "test:1",
Schedulers: &configpb.Schedulers{},
}),
err: "(pool #1 (a)): setting deprecated field `schedulers`",
},
{
cfg: onePool(&configpb.Pool{
Name: []string{"a"},
Realm: "test:1",
BotMonitoring: "bzzz",
}),
err: "(pool #1 (a)): setting deprecated field `bot_monitoring`",
},
{
cfg: onePool(&configpb.Pool{
Realm: "test:1",
}),
err: "(pool #1 (unnamed)): at least one pool name must be given",
},
{
cfg: onePool(&configpb.Pool{
Name: []string{"a", ""},
Realm: "test:1",
}),
err: `(pool #1 (a,)): bad pool name "": the value cannot be empty`,
},
{
cfg: onePool(&configpb.Pool{
Name: []string{"a", "a"},
Realm: "test:1",
}),
err: "(pool #1 (a,a)): pool \"a\" was already declared",
},
{
cfg: onePool(&configpb.Pool{
Name: []string{"a"},
}),
err: "(pool #1 (a)): missing required `realm` field",
},
{
cfg: onePool(&configpb.Pool{
Name: []string{"a"},
Realm: "not-global",
}),
err: "(pool #1 (a)): bad `realm` field: bad global realm name \"not-global\" - should be <project>:<realm>",
},
{
cfg: onePool(&configpb.Pool{
Name: []string{"a"},
Realm: "test:1",
DefaultTaskRealm: "not-global",
}),
err: "(pool #1 (a)): bad `default_task_realm` field: bad global realm name \"not-global\" - should be <project>:<realm>",
},
{
cfg: onePool(&configpb.Pool{
Name: []string{"a"},
Realm: "test:1",
TaskDeploymentScheme: &configpb.Pool_TaskTemplateDeployment{
TaskTemplateDeployment: "b",
},
}),
err: "(pool #1 (a)): unknown `task_template_deployment`: \"b\"",
},
{
cfg: onePool(&configpb.Pool{
Name: []string{"a"},
Realm: "test:1",
TaskDeploymentScheme: &configpb.Pool_TaskTemplateDeploymentInline{
TaskTemplateDeploymentInline: &configpb.TaskTemplateDeployment{
Name: "b",
Prod: &configpb.TaskTemplate{},
},
},
}),
err: "(pool #1 (a) / task_template_deployment_inline): name cannot be specified",
},
// rbe_migration
{
cfg: onePool(&configpb.Pool{
Name: []string{"a"},
Realm: "test:1",
RbeMigration: &configpb.Pool_RBEMigration{
BotModeAllocation: []*configpb.Pool_RBEMigration_BotModeAllocation{
{Mode: configpb.Pool_RBEMigration_BotModeAllocation_RBE, Percent: 100},
},
},
}),
err: "(pool #1 (a) / rbe_migration): rbe_instance is required",
},
{
cfg: onePool(&configpb.Pool{
Name: []string{"a"},
Realm: "test:1",
RbeMigration: &configpb.Pool_RBEMigration{
RbeInstance: "some-instance",
RbeModePercent: 101,
BotModeAllocation: []*configpb.Pool_RBEMigration_BotModeAllocation{
{Mode: configpb.Pool_RBEMigration_BotModeAllocation_RBE, Percent: 100},
},
},
}),
err: "(pool #1 (a) / rbe_migration): rbe_mode_percent should be in [0; 100]",
},
{
cfg: onePool(&configpb.Pool{
Name: []string{"a"},
Realm: "test:1",
RbeMigration: &configpb.Pool_RBEMigration{
RbeInstance: "some-instance",
BotModeAllocation: []*configpb.Pool_RBEMigration_BotModeAllocation{
{Mode: configpb.Pool_RBEMigration_BotModeAllocation_UNKNOWN, Percent: 100},
},
},
}),
err: "(pool #1 (a) / rbe_migration / bot_mode_allocation #0): mode is required",
},
{
cfg: onePool(&configpb.Pool{
Name: []string{"a"},
Realm: "test:1",
RbeMigration: &configpb.Pool_RBEMigration{
RbeInstance: "some-instance",
BotModeAllocation: []*configpb.Pool_RBEMigration_BotModeAllocation{
{Mode: configpb.Pool_RBEMigration_BotModeAllocation_SWARMING, Percent: 20},
{Mode: configpb.Pool_RBEMigration_BotModeAllocation_RBE, Percent: 80},
{Mode: configpb.Pool_RBEMigration_BotModeAllocation_SWARMING, Percent: 10},
},
},
}),
err: "(pool #1 (a) / rbe_migration / bot_mode_allocation #2): allocation for mode SWARMING was already defined",
},
{
cfg: onePool(&configpb.Pool{
Name: []string{"a"},
Realm: "test:1",
RbeMigration: &configpb.Pool_RBEMigration{
RbeInstance: "some-instance",
BotModeAllocation: []*configpb.Pool_RBEMigration_BotModeAllocation{
{Mode: configpb.Pool_RBEMigration_BotModeAllocation_SWARMING, Percent: 101},
},
},
}),
err: "(pool #1 (a) / rbe_migration / bot_mode_allocation #0): percent should be in [0; 100]",
},
{
cfg: onePool(&configpb.Pool{
Name: []string{"a"},
Realm: "test:1",
RbeMigration: &configpb.Pool_RBEMigration{
RbeInstance: "some-instance",
BotModeAllocation: []*configpb.Pool_RBEMigration_BotModeAllocation{
{Mode: configpb.Pool_RBEMigration_BotModeAllocation_SWARMING, Percent: 20},
{Mode: configpb.Pool_RBEMigration_BotModeAllocation_RBE, Percent: 80},
{Mode: configpb.Pool_RBEMigration_BotModeAllocation_HYBRID, Percent: 10},
},
},
}),
err: "(pool #1 (a) / rbe_migration): bot_mode_allocation percents should sum up to 100",
},
}
for _, cs := range testCases {
assert.Loosely(t, call(cs.cfg), should.Match([]string{`in "pools.cfg" ` + cs.err}))
}
})
t.Run("task_template_and_deployment", func(t *ftt.Test) {
testCases := []struct {
name string
cfg *configpb.PoolsCfg
err []string
}{
// task_template
{
name: "template_no_name",
cfg: &configpb.PoolsCfg{
TaskTemplate: []*configpb.TaskTemplate{
{},
},
},
err: []string{"(task_template / #1 ()): name is empty"},
},
{
name: "template_cache",
cfg: &configpb.PoolsCfg{
TaskTemplate: []*configpb.TaskTemplate{
{
Name: "a",
Cache: []*configpb.TaskTemplate_CacheEntry{
{
Name: "a",
},
},
},
},
},
err: []string{"(task_template / #1 (a) / cache): cache path 0: cannot be empty"},
},
{
name: "template_cipd_package",
cfg: &configpb.PoolsCfg{
TaskTemplate: []*configpb.TaskTemplate{
{
Name: "a",
CipdPackage: []*configpb.TaskTemplate_CipdPackage{
{
Pkg: "a",
Path: "c/d",
},
},
},
},
},
err: []string{"(task_template / #1 (a) / cipd_package): package 0 (a): version: required"},
},
{
name: "template_cache_package_conflict_on_path",
cfg: &configpb.PoolsCfg{
TaskTemplate: []*configpb.TaskTemplate{
{
Name: "a",
Cache: []*configpb.TaskTemplate_CacheEntry{
{
Name: "a",
Path: "a/b",
},
},
CipdPackage: []*configpb.TaskTemplate_CipdPackage{
{
Pkg: "a",
Version: "latest",
Path: "a/b",
},
},
},
},
},
err: []string{(`(task_template / #1 (a) / cipd_package):` +
` "a/b": directory has conflicting owners: task_template_cache:a[]` +
` and task_template_cipd_package[a:latest]`)},
},
{
name: "template_env_var_empty",
cfg: &configpb.PoolsCfg{
TaskTemplate: []*configpb.TaskTemplate{
{
Name: "a",
Env: []*configpb.TaskTemplate_Env{
{
Value: "a",
},
},
},
},
},
err: []string{"(task_template / #1 (a) / env / #1 ): var is empty"},
},
{
name: "template_env_too_many",
cfg: &configpb.PoolsCfg{
TaskTemplate: []*configpb.TaskTemplate{
{
Name: "a",
Env: []*configpb.TaskTemplate_Env{
{
Var: "a",
Value: strings.Repeat("a", validate.MaxEnvValueLength+1),
},
},
},
},
},
err: []string{fmt.Sprintf("(task_template / #1 (a) / env / #1 a): value: too long %q: 1025 > 1024", strings.Repeat("a", validate.MaxEnvValueLength+1))},
},
{
name: "template_env_prefix",
cfg: &configpb.PoolsCfg{
TaskTemplate: []*configpb.TaskTemplate{
{
Name: "a",
Env: []*configpb.TaskTemplate_Env{
{
Var: "a",
Prefix: []string{"a/../b"},
},
},
},
},
},
err: []string{`(task_template / #1 (a) / env / #1 a): prefix: "a/../b" is not normalized. Normalized is "b"`},
},
// task template inclusions
{
name: "template_duplicate",
cfg: &configpb.PoolsCfg{
TaskTemplate: []*configpb.TaskTemplate{
{
Name: "a",
},
{
Name: "a",
},
},
},
err: []string{`(task_template / resolve inclusion): template "a" was already declared`},
},
{
name: "template_include_self",
cfg: &configpb.PoolsCfg{
TaskTemplate: []*configpb.TaskTemplate{
{
Name: "a",
Include: []string{
"a",
},
},
},
},
err: []string{`(task_template / resolve inclusion): template "a" includes self`},
},
{
name: "template_include_unknown",
cfg: &configpb.PoolsCfg{
TaskTemplate: []*configpb.TaskTemplate{
{
Name: "a",
Include: []string{
"b",
},
},
},
},
err: []string{`(task_template / resolve inclusion): unknown template "b"`},
},
{
name: "template_include_duplicated",
cfg: &configpb.PoolsCfg{
TaskTemplate: []*configpb.TaskTemplate{
{
Name: "a",
Include: []string{
"b",
"b",
},
},
{
Name: "b",
},
},
},
err: []string{`(task_template / resolve inclusion): template "a" already includes "b"`},
},
{
name: "template_include_cycle",
cfg: &configpb.PoolsCfg{
TaskTemplate: []*configpb.TaskTemplate{
{
Name: "a",
Include: []string{
"b",
},
},
{
Name: "b",
Include: []string{
"c",
},
},
{
Name: "c",
Include: []string{
"a",
},
},
},
},
err: []string{`(task_template / resolve inclusion): encounter inclusion cycle for template "a"`},
},
{
name: "template_diamond_inclusion",
cfg: &configpb.PoolsCfg{
TaskTemplate: []*configpb.TaskTemplate{
{
Name: "a",
Include: []string{
"b",
"c",
},
},
{
Name: "b",
Include: []string{
"c",
},
},
{
Name: "c",
},
},
},
err: []string{`(task_template / resolve inclusion): template "a" already includes "c"`},
},
// conflicts from inclusion
{
name: "template_invalid_after_resolve",
cfg: &configpb.PoolsCfg{
TaskTemplate: []*configpb.TaskTemplate{
{
Name: "a",
Include: []string{
"b",
},
Cache: []*configpb.TaskTemplate_CacheEntry{
{
Name: "a",
Path: "a/b",
},
},
},
{
Name: "b",
CipdPackage: []*configpb.TaskTemplate_CipdPackage{
{
Pkg: "a",
Version: "latest",
Path: "a/b",
},
},
},
},
},
err: []string{
`(task_template / resolve inclusion / #1 (a) / ` +
`cipd_package): "a/b": directory has conflicting owners:` +
` task_template_cache:a[] and task_template_cipd_package[a:latest]`},
},
// task_template_deployment
{
name: "deployment_no_name",
cfg: &configpb.PoolsCfg{
TaskTemplateDeployment: []*configpb.TaskTemplateDeployment{
{
Prod: &configpb.TaskTemplate{},
},
},
},
err: []string{"(task_template_deployment / #1 ()): name is empty"},
},
{
name: "deployment_duplicate",
cfg: &configpb.PoolsCfg{
TaskTemplateDeployment: []*configpb.TaskTemplateDeployment{
{
Name: "a",
Prod: &configpb.TaskTemplate{},
},
{
Name: "a",
Prod: &configpb.TaskTemplate{},
},
},
},
err: []string{`(task_template_deployment / #2 (a)): deployment "a" was already declared`},
},
{
name: "deployment_no_prod",
cfg: &configpb.PoolsCfg{
TaskTemplateDeployment: []*configpb.TaskTemplateDeployment{
{
Name: "a",
},
},
},
err: []string{"(task_template_deployment / #1 (a) / prod): required"},
},
{
name: "deployment_canary_chance_without_canary",
cfg: &configpb.PoolsCfg{
TaskTemplateDeployment: []*configpb.TaskTemplateDeployment{
{
Name: "a",
CanaryChance: 1000,
Prod: &configpb.TaskTemplate{},
},
},
},
err: []string{`(task_template_deployment / #1 (a)): canary_chance specified without a canary`},
},
{
name: "deployment_canary_chance_out_of_range",
cfg: &configpb.PoolsCfg{
TaskTemplateDeployment: []*configpb.TaskTemplateDeployment{
{
Name: "a",
CanaryChance: 10000,
Prod: &configpb.TaskTemplate{},
Canary: &configpb.TaskTemplate{},
},
},
},
err: []string{`(task_template_deployment / #1 (a)): canary_chance out of range [0,9999]`},
},
{
name: "deployment_prod_with_name",
cfg: &configpb.PoolsCfg{
TaskTemplateDeployment: []*configpb.TaskTemplateDeployment{
{
Name: "a",
Prod: &configpb.TaskTemplate{
Name: "b",
},
},
},
},
err: []string{`(task_template_deployment / #1 (a) / prod): name cannot be specified`},
},
{
name: "deployment_prod_include_unknown_template",
cfg: &configpb.PoolsCfg{
TaskTemplateDeployment: []*configpb.TaskTemplateDeployment{
{
Name: "a",
Prod: &configpb.TaskTemplate{
Include: []string{
"unknown",
},
},
},
},
},
err: []string{`(task_template_deployment / #1 (a) / prod): includes unknown template "unknown"`},
},
{
name: "deployment_prod_include_duplicated_template",
cfg: &configpb.PoolsCfg{
TaskTemplate: []*configpb.TaskTemplate{
{
Name: "t1",
Include: []string{
"t2",
},
},
{
Name: "t2",
},
},
TaskTemplateDeployment: []*configpb.TaskTemplateDeployment{
{
Name: "a",
Prod: &configpb.TaskTemplate{
Include: []string{
"t1",
"t2",
},
},
},
},
},
err: []string{`(task_template_deployment / #1 (a) / prod): template already includes "t2"`},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *ftt.Test) {
tc.cfg.DefaultExternalServices = &configpb.ExternalServices{
Cipd: &configpb.ExternalServices_CIPD{
Server: "https://cipd.example.com",
ClientPackage: &configpb.CipdPackage{
PackageName: "client/pkg",
Version: "latest",
},
},
}
for i := range tc.err {
tc.err[i] = `in "pools.cfg" ` + tc.err[i]
}
assert.Loosely(t, call(tc.cfg), should.Match(tc.err))
})
}
})
})
}
func TestPoolRBEConfig(t *testing.T) {
t.Parallel()
call := func(botID string, cfg *configpb.Pool_RBEMigration) RBEConfig {
pool, err := newPool(&configpb.Pool{
Name: []string{"pool"},
RbeMigration: cfg,
}, nil, nil)
if err != nil {
t.Fatal(err)
}
return pool.rbeConfig(botID)
}
t.Run("edge cases", func(t *testing.T) {
assert.That(t, call("bot", nil), should.Equal(RBEConfig{
Mode: configpb.Pool_RBEMigration_BotModeAllocation_SWARMING,
}))
allRBE := call("bot", &configpb.Pool_RBEMigration{
RbeInstance: "some-instance",
BotModeAllocation: []*configpb.Pool_RBEMigration_BotModeAllocation{
{Mode: configpb.Pool_RBEMigration_BotModeAllocation_RBE, Percent: 100},
},
})
assert.That(t, allRBE, should.Equal(RBEConfig{
Instance: "some-instance",
Mode: configpb.Pool_RBEMigration_BotModeAllocation_RBE,
}))
})
t.Run("distribution", func(t *testing.T) {
perMode := map[configpb.Pool_RBEMigration_BotModeAllocation_BotMode]int{}
for i := range 1000 {
cfg := call(fmt.Sprintf("bot-%d", i), &configpb.Pool_RBEMigration{
RbeInstance: "some-instance",
BotModeAllocation: []*configpb.Pool_RBEMigration_BotModeAllocation{
{Mode: configpb.Pool_RBEMigration_BotModeAllocation_SWARMING, Percent: 20},
{Mode: configpb.Pool_RBEMigration_BotModeAllocation_HYBRID, Percent: 10},
{Mode: configpb.Pool_RBEMigration_BotModeAllocation_RBE, Percent: 70},
},
})
perMode[cfg.Mode] += 1
}
assert.That(t, perMode, should.Match(map[configpb.Pool_RBEMigration_BotModeAllocation_BotMode]int{
configpb.Pool_RBEMigration_BotModeAllocation_SWARMING: 216,
configpb.Pool_RBEMigration_BotModeAllocation_HYBRID: 108,
configpb.Pool_RBEMigration_BotModeAllocation_RBE: 676,
}))
})
}
func TestNewInclusionGraph(t *testing.T) {
t.Parallel()
ftt.Run("newInclusionGraph", t, func(t *ftt.Test) {
inclusions := func(g *inclusionGraph) map[string][]string {
res := make(map[string][]string, len(g.flattened))
for name, node := range g.flattened {
res[name] = node.tmp.Include
}
return res
}
testCases := []struct {
name string
cfg *configpb.PoolsCfg
res map[string][]string
}{
{
name: "empty",
cfg: &configpb.PoolsCfg{},
res: map[string][]string{},
},
{
name: "single",
cfg: &configpb.PoolsCfg{
TaskTemplate: []*configpb.TaskTemplate{
{
Name: "t",
},
},
},
res: map[string][]string{
"t": {"t"},
},
},
{
name: "simple",
cfg: &configpb.PoolsCfg{
TaskTemplate: []*configpb.TaskTemplate{
{
Name: "t1",
Include: []string{
"t2",
},
},
{
Name: "t2",
},
},
},
res: map[string][]string{
"t1": {"t1", "t2"},
"t2": {"t2"},
},
},
{
name: "tree",
cfg: &configpb.PoolsCfg{
TaskTemplate: []*configpb.TaskTemplate{
{
Name: "t1",
Include: []string{
"t2",
"t3",
},
},
{
Name: "t2",
Include: []string{
"t5",
},
},
{
Name: "t3",
Include: []string{
"t4",
},
},
{
Name: "t4",
},
{
Name: "t5",
},
},
},
res: map[string][]string{
"t1": {"t1", "t2", "t5", "t3", "t4"},
"t2": {"t2", "t5"},
"t3": {"t3", "t4"},
"t4": {"t4"},
"t5": {"t5"},
},
},
{
name: "forest",
cfg: &configpb.PoolsCfg{
TaskTemplate: []*configpb.TaskTemplate{
{
Name: "t1",
Include: []string{
"t3",
},
},
{
Name: "t2",
Include: []string{
"t4",
},
},
{
Name: "t3",
Include: []string{
"t4",
},
},
{
Name: "t4",
},
},
},
res: map[string][]string{
"t1": {"t1", "t3", "t4"},
"t2": {"t2", "t4"},
"t3": {"t3", "t4"},
"t4": {"t4"},
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *ftt.Test) {
graph, merr := newInclusionGraph(tc.cfg.TaskTemplate)
assert.That(t, merr.AsError(), should.ErrLike(nil))
assert.Loosely(t, inclusions(graph), should.Match(tc.res))
})
}
})
}
func TestResolveTemplates(t *testing.T) {
t.Parallel()
ftt.Run("Works", t, func(t *ftt.Test) {
t.Run("simple inclusion", func(t *ftt.Test) {
cfg := &configpb.PoolsCfg{
TaskTemplate: []*configpb.TaskTemplate{
{
Name: "a",
Include: []string{
"b",
},
},
{
Name: "b",
Cache: goodTaskTemplate.Cache,
CipdPackage: goodTaskTemplate.CipdPackage,
Env: goodTaskTemplate.Env,
},
{
Name: "c",
Include: []string{
"b",
},
},
},
}
graph, merr := newInclusionGraph(cfg.TaskTemplate)
assert.That(t, merr.AsError(), should.ErrLike(nil))
err := graph.flattenTaskTemplates()
assert.That(t, err, should.ErrLike(nil))
tmpMap := graph.flattened
assert.That(t, tmpMap["a"].tmp.Cache, should.Match(goodTaskTemplate.Cache))
assert.That(t, tmpMap["a"].tmp.CipdPackage, should.Match(goodTaskTemplate.CipdPackage))
assert.That(t, tmpMap["a"].tmp.Env, should.Match(goodTaskTemplate.Env))
assert.That(t, tmpMap["c"].tmp.Cache, should.Match(goodTaskTemplate.Cache))
assert.That(t, tmpMap["c"].tmp.CipdPackage, should.Match(goodTaskTemplate.CipdPackage))
assert.That(t, tmpMap["c"].tmp.Env, should.Match(goodTaskTemplate.Env))
})
t.Run("Add fields from included templates", func(t *ftt.Test) {
cfg := &configpb.PoolsCfg{
TaskTemplate: []*configpb.TaskTemplate{
{
Name: "a",
Include: []string{
"b",
},
Cache: []*configpb.TaskTemplate_CacheEntry{
{
Name: "c1",
Path: "a/b",
},
},
CipdPackage: []*configpb.TaskTemplate_CipdPackage{
{
Pkg: "p1",
Version: "latest",
Path: "c/d",
},
},
Env: []*configpb.TaskTemplate_Env{
{
Var: "ev1",
Value: "ev1",
Prefix: []string{
"e/f",
},
Soft: true,
},
},
},
{
Name: "b",
Cache: []*configpb.TaskTemplate_CacheEntry{
{
Name: "c2",
Path: "a/c",
},
},
CipdPackage: []*configpb.TaskTemplate_CipdPackage{
{
Pkg: "p2",
Version: "latest",
Path: "c/f",
},
},
Env: []*configpb.TaskTemplate_Env{
{
Var: "ev2",
Value: "ev2",
Prefix: []string{
"e/f",
},
Soft: true,
},
},
},
},
}
graph, merr := newInclusionGraph(cfg.TaskTemplate)
assert.That(t, merr.AsError(), should.ErrLike(nil))
err := graph.flattenTaskTemplates()
assert.That(t, err, should.ErrLike(nil))
tmpMap := graph.flattened
expected := &configpb.TaskTemplate{
Name: "a",
Include: []string{
"a",
"b",
},
Cache: []*configpb.TaskTemplate_CacheEntry{
{
Name: "c1",
Path: "a/b",
},
{
Name: "c2",
Path: "a/c",
},
},
CipdPackage: []*configpb.TaskTemplate_CipdPackage{
{
Pkg: "p1",
Version: "latest",
Path: "c/d",
},
{
Pkg: "p2",
Version: "latest",
Path: "c/f",
},
},
Env: []*configpb.TaskTemplate_Env{
{
Var: "ev1",
Value: "ev1",
Prefix: []string{
"e/f",
},
Soft: true,
},
{
Var: "ev2",
Value: "ev2",
Prefix: []string{
"e/f",
},
Soft: true,
},
},
}
assert.That(t, tmpMap["a"].tmp, should.Match(expected))
assert.That(t, tmpMap["b"].tmp.Include, should.Match([]string{"b"}))
})
t.Run("override fields from included templates", func(t *ftt.Test) {
cfg := &configpb.PoolsCfg{
TaskTemplate: []*configpb.TaskTemplate{
{
Name: "a",
Include: []string{
"b",
},
Cache: []*configpb.TaskTemplate_CacheEntry{
{
Name: "c1",
Path: "a/b",
},
},
CipdPackage: []*configpb.TaskTemplate_CipdPackage{
{
Pkg: "p1",
Version: "prod",
Path: "c/d",
},
{
Pkg: "p2",
Version: "prod",
Path: "c/f",
},
},
Env: []*configpb.TaskTemplate_Env{
{
Var: "ev1",
Value: "ev1",
Prefix: []string{
"e/f",
},
Soft: true,
},
},
},
{
Name: "b",
Cache: []*configpb.TaskTemplate_CacheEntry{
{
Name: "c1",
Path: "a/c",
},
},
CipdPackage: []*configpb.TaskTemplate_CipdPackage{
// Same (Pkg, Path) as base, will be overridden.
{
Pkg: "p1",
Version: "latest",
Path: "c/d",
},
// Same Pkg different Path from base, will not be
// overridden.
{
Pkg: "p2",
Version: "prod",
Path: "c/g",
},
},
Env: []*configpb.TaskTemplate_Env{
{
Var: "ev1",
Value: "ev2",
Prefix: []string{
"e/g",
},
Soft: true,
},
},
},
},
}
graph, merr := newInclusionGraph(cfg.TaskTemplate)
assert.That(t, merr.AsError(), should.ErrLike(nil))
err := graph.flattenTaskTemplates()
assert.That(t, err, should.ErrLike(nil))
tmpMap := graph.flattened
expected := &configpb.TaskTemplate{
Name: "a",
Include: []string{
"a",
"b",
},
Cache: []*configpb.TaskTemplate_CacheEntry{
{
Name: "c1",
Path: "a/b",
},
},
CipdPackage: []*configpb.TaskTemplate_CipdPackage{
{
Pkg: "p1",
Version: "prod",
Path: "c/d",
},
{
Pkg: "p2",
Version: "prod",
Path: "c/f",
},
{
Pkg: "p2",
Version: "prod",
Path: "c/g",
},
},
Env: []*configpb.TaskTemplate_Env{
{
Var: "ev1",
Value: "ev1",
Prefix: []string{
"e/f",
"e/g",
},
Soft: true,
},
},
}
assert.That(t, tmpMap["a"].tmp, should.Match(expected))
})
t.Run("merge_from_multiple_templates", func(t *ftt.Test) {
cfg := &configpb.PoolsCfg{
TaskTemplate: []*configpb.TaskTemplate{
{
Name: "a",
Include: []string{
"b",
"c",
},
},
{
Name: "b",
Include: []string{
"d",
},
Cache: []*configpb.TaskTemplate_CacheEntry{
{
Name: "c1",
Path: "a/b",
},
},
},
{
Name: "c",
Cache: []*configpb.TaskTemplate_CacheEntry{
{
Name: "c1",
Path: "a/c",
},
},
},
{
Name: "d",
Cache: []*configpb.TaskTemplate_CacheEntry{
{
Name: "c1",
Path: "a/d",
},
{
Name: "c2",
Path: "a/d2",
},
},
},
},
}
graph, merr := newInclusionGraph(cfg.TaskTemplate)
assert.That(t, merr.AsError(), should.ErrLike(nil))
err := graph.flattenTaskTemplates()
assert.That(t, err, should.ErrLike(nil))
tmpMap := graph.flattened
expected := &configpb.TaskTemplate{
Name: "a",
Include: []string{
"a",
"b",
"d",
"c",
},
Cache: []*configpb.TaskTemplate_CacheEntry{
{
Name: "c1",
Path: "a/b",
},
{
Name: "c2",
Path: "a/d2",
},
},
}
assert.That(t, tmpMap["a"].tmp, should.Match(expected))
})
})
}