github.com/GoogleCloudPlatform/testgrid@v0.0.174/config/converge.go (about) 1 /* 2 Copyright 2021 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 // Package config merges together multiple configurations. 18 package config 19 20 import ( 21 "fmt" 22 "sort" 23 24 configpb "github.com/GoogleCloudPlatform/testgrid/pb/config" 25 "github.com/golang/protobuf/proto" 26 ) 27 28 // Zero-memory member for hash sets 29 type void struct{} 30 31 var insert void 32 33 // Converge merges together the Configurations given in the map set. 34 // If there are duplicate entries, the string key may be added as a prefix to 35 // maintain uniqueness for Dashboards, DashboardGroups, and TestGroups. 36 // The config at key "" will not be modified. 37 // 38 // The output protobuf will pass config.Validate if all its inputs pass config.Validate 39 func Converge(shards map[string]*configpb.Configuration) (*configpb.Configuration, error) { 40 if len(shards) == 0 { 41 return nil, fmt.Errorf("No configurations to converge") 42 } 43 44 result := configpb.Configuration{} 45 46 // Go through shards in key string order, so the prefix results are predictable and alphabetical. 47 var keys []string 48 for key := range shards { 49 keys = append(keys, key) 50 } 51 sort.Strings(keys) 52 53 // Dashboards and Dashboard Groups can't share names with each other 54 DashboardsAndGroups := make(map[string]void) 55 TestGroups := make(map[string]void) 56 57 for _, key := range keys { 58 cfg := shards[key] 59 NewDashboardsAndGroups := make(map[string]void) 60 NewTestGroups := make(map[string]void) 61 for _, testgroup := range cfg.TestGroups { 62 NewTestGroups[testgroup.Name] = insert 63 } 64 for _, dashboard := range cfg.Dashboards { 65 NewDashboardsAndGroups[dashboard.Name] = insert 66 } 67 for _, dashboardGroup := range cfg.DashboardGroups { 68 NewDashboardsAndGroups[dashboardGroup.Name] = insert 69 } 70 71 dashboardRenames := negotiateConversions(key, DashboardsAndGroups, NewDashboardsAndGroups) 72 testGroupRenames := negotiateConversions(key, TestGroups, NewTestGroups) 73 74 for olddash, newdash := range dashboardRenames { 75 RenameDashboardOrGroup(olddash, newdash, cfg) 76 } 77 78 for oldtest, newtest := range testGroupRenames { 79 RenameTestGroup(oldtest, newtest, cfg) 80 } 81 82 // Merge protos and cached sets 83 proto.Merge(&result, cfg) 84 85 for _, dash := range cfg.Dashboards { 86 DashboardsAndGroups[dash.Name] = insert 87 } 88 for _, group := range cfg.DashboardGroups { 89 DashboardsAndGroups[group.Name] = insert 90 } 91 for _, test := range cfg.TestGroups { 92 TestGroups[test.Name] = insert 93 } 94 } 95 96 return &result, nil 97 } 98 99 // Given two sets of strings, returns a "conversions" for each duplicate. 100 // If there are no duplicates, returns a zero-length map. 101 // If there are duplicates, returns a map of the string in "new" -> the string it should become. 102 // Will try "prefix-string" first, then "prefix-2-string", etc. 103 func negotiateConversions(prefix string, original, new map[string]void) map[string]string { 104 if len(original) == 0 || len(new) == 0 { 105 return nil 106 } 107 108 result := make(map[string]string) 109 110 for key := range new { 111 if _, exists := original[key]; exists { 112 candidate := fmt.Sprintf("%s-%s", prefix, key) 113 attempt := 1 114 for true { 115 _, existInOld := original[candidate] 116 _, existInNew := new[candidate] 117 if !existInOld && !existInNew { 118 result[key] = candidate 119 break 120 } 121 attempt++ 122 candidate = fmt.Sprintf("%s-%d-%s", prefix, attempt, key) 123 } 124 } 125 } 126 127 return result 128 } 129 130 // RenameTestGroup renames all references to TestGroup 'original' to 'new'. 131 // Does not verify if the new name is already taken. 132 func RenameTestGroup(original, new string, cfg *configpb.Configuration) *configpb.Configuration { 133 // If a TestGroup is changed, it needs to be changed for tabs that reference it also 134 for _, testGroup := range cfg.TestGroups { 135 if testGroup.Name == original { 136 testGroup.Name = new 137 } 138 } 139 for _, dashboard := range cfg.Dashboards { 140 for _, dashTab := range dashboard.DashboardTab { 141 if dashTab.TestGroupName == original { 142 dashTab.TestGroupName = new 143 } 144 } 145 } 146 return cfg 147 } 148 149 // RenameDashboardOrGroup renames all Dashboards and DashboardGroups named 'original' to 'new'. 150 // Since Dashboards and Dashboard Groups can't share names with each other, a valid Configuration will rename at most one item. 151 func RenameDashboardOrGroup(original, new string, cfg *configpb.Configuration) *configpb.Configuration { 152 return RenameDashboard(original, new, RenameDashboardGroup(original, new, cfg)) 153 } 154 155 // RenameDashboard renames all references to Dashboard 'original' to 'new'. 156 // Does not verify if the new name is already taken. 157 func RenameDashboard(original, new string, cfg *configpb.Configuration) *configpb.Configuration { 158 // If a dashboard is renamed, it needs to be changed for DashboardGroups that reference it also 159 for _, dashboard := range cfg.Dashboards { 160 if dashboard.Name == original { 161 dashboard.Name = new 162 } 163 } 164 165 for _, dashgroup := range cfg.DashboardGroups { 166 for i, dashboard := range dashgroup.DashboardNames { 167 if dashboard == original { 168 dashgroup.DashboardNames[i] = new 169 } 170 } 171 } 172 173 return cfg 174 } 175 176 // RenameDashboardGroup renames all references to DashboardGroup 'original' to 'new'. 177 // Does not verify if the new name is already taken. 178 func RenameDashboardGroup(original, new string, cfg *configpb.Configuration) *configpb.Configuration { 179 for _, dashgroup := range cfg.DashboardGroups { 180 if dashgroup.Name == original { 181 dashgroup.Name = new 182 } 183 } 184 185 return cfg 186 }