github.com/GoogleCloudPlatform/testgrid@v0.0.174/config/yamlcfg/yaml2proto.go (about) 1 /* 2 Copyright 2016 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 yamlcfg 18 19 import ( 20 "errors" 21 "fmt" 22 "io/ioutil" 23 "os" 24 "path/filepath" 25 26 cfgutil "github.com/GoogleCloudPlatform/testgrid/config" 27 "github.com/GoogleCloudPlatform/testgrid/pb/config" 28 "sigs.k8s.io/yaml" 29 ) 30 31 // getDefaults take all paths found through seeking, returns list of dirs with defaults 32 func getDefaults(allPaths []string) (defaults []string, err error) { 33 dirsFound := make(map[string]bool) 34 for _, path := range allPaths { 35 if filepath.Base(path) == "default.yaml" || filepath.Base(path) == "default.yml" { 36 if _, ok := dirsFound[filepath.Dir(path)]; ok { 37 return nil, fmt.Errorf("two default files found in dir %q", filepath.Dir(path)) 38 } 39 defaults = append(defaults, path) 40 dirsFound[filepath.Dir(path)] = true 41 } 42 } 43 return defaults, nil 44 } 45 46 // seekDefaults finds all default files and returns a map of directory to its default contents. 47 // TODO: Implement filesystem fake in order to unit test this better. 48 func seekDefaults(paths []string) (map[string]DefaultConfiguration, error) { 49 defaultFiles := make(map[string]DefaultConfiguration) 50 var allPaths []string 51 err := SeekYAMLFiles(paths, func(path string, info os.FileInfo) error { 52 allPaths = append(allPaths, path) 53 return nil 54 }) 55 if err != nil { 56 return nil, fmt.Errorf("unable to walk paths, %v", err) 57 } 58 defaults, err := getDefaults(allPaths) 59 if err != nil { 60 return nil, fmt.Errorf("unable to get defaults, %v", err) 61 } 62 for _, path := range defaults { 63 b, err := ioutil.ReadFile(path) 64 if err != nil { 65 return nil, fmt.Errorf("failed to read default at %s: %v", path, err) 66 } 67 curDefault, err := LoadDefaults(b) 68 if err != nil { 69 return nil, fmt.Errorf("failed to deserialize default at %s: %v", path, err) 70 } 71 defaultFiles[filepath.Dir(path)] = curDefault 72 } 73 return defaultFiles, nil 74 } 75 76 // pathDefault returns the closest DefaultConfiguration for a path (default in path's dir, or overall default). 77 func pathDefault(path string, defaultFiles map[string]DefaultConfiguration, defaults DefaultConfiguration) DefaultConfiguration { 78 if localDefaults, ok := defaultFiles[filepath.Dir(path)]; ok { 79 return localDefaults 80 } 81 return defaults 82 } 83 84 // ReadConfig takes multiple source paths of the following form: 85 // If path is a local file, then the file will be parsed as YAML 86 // If path is a directory, then all files and directories within it will be parsed. 87 // If this directory has a default(s).yaml file, apply it to all configured entities, 88 // after applying defaults from defaultPath. 89 // Optionally, defaultPath points to default setting YAML 90 // Returns a configuration proto containing the data from all of those sources 91 func ReadConfig(paths []string, defaultpath string, strict bool) (config.Configuration, error) { 92 93 var result config.Configuration 94 95 // Get the overall default file, if specified. 96 var defaults DefaultConfiguration 97 if defaultpath != "" { 98 b, err := ioutil.ReadFile(defaultpath) 99 if err != nil { 100 return result, fmt.Errorf("failed to read default at %s: %v", defaultpath, err) 101 } 102 defaults, err = LoadDefaults(b) 103 if err != nil { 104 return result, fmt.Errorf("failed to deserialize default at %s: %v", defaultpath, err) 105 } 106 } 107 108 // Find all default files, map their directory to their contents. 109 defaultFiles, err := seekDefaults(paths) 110 if err != nil { 111 return result, err 112 } 113 114 // Gather configuration from each YAML file, applying the config's default.yaml if 115 // one exists in its directory, or the overall default otherwise. 116 err = SeekYAMLFiles(paths, func(path string, info os.FileInfo) error { 117 if filepath.Base(path) == "default.yaml" || filepath.Base(path) == "default.yml" { 118 return nil 119 } 120 // Read YAML file and Update config 121 b, err := ioutil.ReadFile(path) 122 if err != nil { 123 return fmt.Errorf("failed to read %s: %v", path, err) 124 } 125 localDefaults := pathDefault(path, defaultFiles, defaults) 126 if err = Update(&result, b, &localDefaults, strict); err != nil { 127 return fmt.Errorf("failed to merge %s into config: %v", path, err) 128 } 129 return nil 130 }) 131 if err != nil { 132 return result, fmt.Errorf("SeekYAMLFiles(%v), gathering config: %v", paths, err) 133 } 134 135 return result, err 136 } 137 138 // Update reads the config in yamlData and updates the config in c. 139 // If reconcile is non-nil, it will pad out new entries with those default settings 140 func Update(cfg *config.Configuration, yamlData []byte, reconcile *DefaultConfiguration, strict bool) error { 141 142 newConfig := &config.Configuration{} 143 if strict { 144 if err := yaml.UnmarshalStrict(yamlData, newConfig); err != nil { 145 return err 146 } 147 } else { 148 if err := yaml.Unmarshal(yamlData, newConfig); err != nil { 149 return err 150 } 151 } 152 153 if cfg == nil { 154 cfg = &config.Configuration{} 155 } 156 157 for _, testgroup := range newConfig.TestGroups { 158 if reconcile != nil { 159 ReconcileTestGroup(testgroup, reconcile.DefaultTestGroup) 160 } 161 cfg.TestGroups = append(cfg.TestGroups, testgroup) 162 } 163 164 for _, dashboard := range newConfig.Dashboards { 165 if reconcile != nil { 166 for _, dashboardtab := range dashboard.DashboardTab { 167 ReconcileDashboardTab(dashboardtab, reconcile.DefaultDashboardTab) 168 } 169 } 170 cfg.Dashboards = append(cfg.Dashboards, dashboard) 171 } 172 173 for _, dashboardGroup := range newConfig.DashboardGroups { 174 cfg.DashboardGroups = append(cfg.DashboardGroups, dashboardGroup) 175 } 176 177 return nil 178 } 179 180 // MarshalYAML returns a YAML file representing the parsed configuration. 181 // Returns an error if config is invalid or encoding failed. 182 func MarshalYAML(c *config.Configuration) ([]byte, error) { 183 if c == nil { 184 return nil, errors.New("got an empty config.Configuration") 185 } 186 if err := cfgutil.Validate(c); err != nil { 187 return nil, err 188 } 189 bytes, err := yaml.Marshal(c) 190 if err != nil { 191 return nil, fmt.Errorf("could not write config to yaml: %v", err) 192 } 193 return bytes, nil 194 } 195 196 // DefaultConfiguration describes a default configuration that should be applied before other configs. 197 type DefaultConfiguration struct { 198 // A default testgroup with default initialization data 199 DefaultTestGroup *config.TestGroup `json:"default_test_group,omitempty"` 200 // A default dashboard tab with default initialization data 201 DefaultDashboardTab *config.DashboardTab `json:"default_dashboard_tab,omitempty"` 202 } 203 204 // MissingFieldError is an error that includes the missing field. 205 type MissingFieldError struct { 206 Field string 207 } 208 209 func (e MissingFieldError) Error() string { 210 return fmt.Sprintf("field missing or unset: %s", e.Field) 211 } 212 213 // ReconcileTestGroup sets unfilled currentTestGroup fields to the corresponding defaultTestGroup value, if present 214 func ReconcileTestGroup(currentTestGroup *config.TestGroup, defaultTestGroup *config.TestGroup) { 215 if currentTestGroup.DaysOfResults == 0 { 216 currentTestGroup.DaysOfResults = defaultTestGroup.DaysOfResults 217 } 218 219 if currentTestGroup.TestsNamePolicy == config.TestGroup_TESTS_NAME_UNSPECIFIED { 220 currentTestGroup.TestsNamePolicy = defaultTestGroup.TestsNamePolicy 221 } 222 223 if currentTestGroup.IgnorePending == false { 224 currentTestGroup.IgnorePending = defaultTestGroup.IgnorePending 225 } 226 227 if currentTestGroup.IgnoreSkip == false { 228 currentTestGroup.IgnoreSkip = defaultTestGroup.IgnoreSkip 229 } 230 231 if currentTestGroup.ColumnHeader == nil { 232 currentTestGroup.ColumnHeader = defaultTestGroup.ColumnHeader 233 } 234 235 if currentTestGroup.NumColumnsRecent == 0 { 236 currentTestGroup.NumColumnsRecent = defaultTestGroup.NumColumnsRecent 237 } 238 239 if currentTestGroup.AlertStaleResultsHours == 0 { 240 currentTestGroup.AlertStaleResultsHours = defaultTestGroup.AlertStaleResultsHours 241 } 242 243 if currentTestGroup.NumFailuresToAlert == 0 { 244 currentTestGroup.NumFailuresToAlert = defaultTestGroup.NumFailuresToAlert 245 } 246 if currentTestGroup.CodeSearchPath == "" { 247 currentTestGroup.CodeSearchPath = defaultTestGroup.CodeSearchPath 248 } 249 if currentTestGroup.NumPassesToDisableAlert == 0 { 250 currentTestGroup.NumPassesToDisableAlert = defaultTestGroup.NumPassesToDisableAlert 251 } 252 // is_external and user_kubernetes_client should always be true 253 currentTestGroup.IsExternal = true 254 currentTestGroup.UseKubernetesClient = true 255 } 256 257 // ReconcileDashboardTab sets unfilled currentTab fields to the corresponding defaultTab value, if present 258 func ReconcileDashboardTab(currentTab *config.DashboardTab, defaultTab *config.DashboardTab) { 259 if currentTab.BugComponent == 0 { 260 currentTab.BugComponent = defaultTab.BugComponent 261 } 262 263 if currentTab.CodeSearchPath == "" { 264 currentTab.CodeSearchPath = defaultTab.CodeSearchPath 265 } 266 267 if currentTab.NumColumnsRecent == 0 { 268 currentTab.NumColumnsRecent = defaultTab.NumColumnsRecent 269 } 270 271 if currentTab.OpenTestTemplate == nil { 272 currentTab.OpenTestTemplate = defaultTab.OpenTestTemplate 273 } 274 275 if currentTab.FileBugTemplate == nil { 276 currentTab.FileBugTemplate = defaultTab.FileBugTemplate 277 } 278 279 if currentTab.AttachBugTemplate == nil { 280 currentTab.AttachBugTemplate = defaultTab.AttachBugTemplate 281 } 282 283 if currentTab.ResultsText == "" { 284 currentTab.ResultsText = defaultTab.ResultsText 285 } 286 287 if currentTab.ResultsUrlTemplate == nil { 288 currentTab.ResultsUrlTemplate = defaultTab.ResultsUrlTemplate 289 } 290 291 if currentTab.CodeSearchUrlTemplate == nil { 292 currentTab.CodeSearchUrlTemplate = defaultTab.CodeSearchUrlTemplate 293 } 294 295 if currentTab.AlertOptions == nil { 296 currentTab.AlertOptions = defaultTab.AlertOptions 297 } 298 299 if currentTab.OpenBugTemplate == nil { 300 currentTab.OpenBugTemplate = defaultTab.OpenBugTemplate 301 } 302 } 303 304 // LoadDefaults reads and validates default settings from YAML 305 // Returns an error if the defaultConfig is partially or completely missing. 306 func LoadDefaults(yamlData []byte) (DefaultConfiguration, error) { 307 308 var result DefaultConfiguration 309 err := yaml.Unmarshal(yamlData, &result) 310 if err != nil { 311 return result, err 312 } 313 314 if result.DefaultTestGroup == nil { 315 return result, MissingFieldError{"DefaultTestGroup"} 316 } 317 if result.DefaultDashboardTab == nil { 318 return result, MissingFieldError{"DefaultDashboardTab"} 319 } 320 return result, nil 321 } 322 323 // SeekYAMLFiles walks through paths and directories, calling the passed function on each YAML file 324 // future modifications to what Configurator sees as a "config file" can be made here 325 func SeekYAMLFiles(paths []string, callFunc func(path string, info os.FileInfo) error) error { 326 for _, path := range paths { 327 _, err := os.Stat(path) 328 if err != nil { 329 return fmt.Errorf("Failed status call on %s: %v", path, err) 330 } 331 332 err = filepath.Walk(path, func(path string, info os.FileInfo, err error) error { 333 334 // A bad file should not stop us from parsing the directory 335 if err != nil { 336 return nil 337 } 338 339 // Only YAML files will be parsed 340 if filepath.Ext(path) != ".yaml" && filepath.Ext(path) != ".yml" { 341 return nil 342 } 343 344 if info.IsDir() { 345 return nil 346 } 347 348 return callFunc(path, info) 349 }) 350 351 if err != nil { 352 return fmt.Errorf("Failed to walk through %s: %v", path, err) 353 } 354 } 355 return nil 356 }