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  }