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  }