istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pkg/test/framework/config/config.go (about)

     1  // Copyright Istio Authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package config
    16  
    17  import (
    18  	"flag"
    19  	"fmt"
    20  	"os"
    21  	"strings"
    22  
    23  	"go.uber.org/atomic"
    24  	"gopkg.in/yaml.v3"
    25  
    26  	"istio.io/istio/pkg/test/scopes"
    27  	"istio.io/istio/pkg/test/util/file"
    28  )
    29  
    30  const prefix = "istio.test"
    31  
    32  var (
    33  	configFilePath string
    34  	parsed         atomic.Bool
    35  )
    36  
    37  func init() {
    38  	flag.StringVar(&configFilePath, "istio.test.config", "", "Path to test framework config file")
    39  }
    40  
    41  type Value interface {
    42  	flag.Value
    43  	// SetConfig will receive either a Map or a []Map
    44  	SetConfig(any) error
    45  }
    46  
    47  func Parsed() bool {
    48  	return flag.Parsed() && parsed.Load()
    49  }
    50  
    51  // Parse overrides any unset command line flags beginning with "istio.test" with values provided
    52  // from a YAML file.
    53  func Parse() {
    54  	defer func() {
    55  		parsed.Store(true)
    56  	}()
    57  	if !flag.Parsed() {
    58  		flag.Parse()
    59  	}
    60  	if configFilePath == "" {
    61  		return
    62  	}
    63  
    64  	cfg, err := readConfig()
    65  	if err != nil {
    66  		scopes.Framework.Error(err)
    67  		return
    68  	}
    69  	set := map[string]struct{}{}
    70  	flag.Visit(func(f *flag.Flag) {
    71  		set[f.Name] = struct{}{}
    72  	})
    73  
    74  	flag.VisitAll(func(f *flag.Flag) {
    75  		var err error
    76  		defer func() {
    77  			if err != nil {
    78  				scopes.Framework.Errorf("failed getting %s from config file: %v", f.Name, err)
    79  			}
    80  		}()
    81  
    82  		// exclude non-istio flags and flags that were set via command line
    83  		if !strings.HasPrefix(f.Name, prefix) {
    84  			return
    85  		}
    86  		if _, ok := set[f.Name]; ok {
    87  			return
    88  		}
    89  
    90  		// grab the map containing the last "." separated key
    91  		keys := strings.Split(f.Name, ".")
    92  		parentPath, key := keys[:len(keys)-1], keys[len(keys)-1]
    93  		parent := cfg
    94  		for _, k := range parentPath {
    95  			parent = parent.Map(k)
    96  			if parent == nil {
    97  				return
    98  			}
    99  		}
   100  
   101  		// if the registered flag implements config.Value, and is a non-string type, we can do fancy custom parsing
   102  		cfgValue, isCfgVal := f.Value.(Value)
   103  		if cfgMap := parent.Map(key); isCfgVal && len(cfgMap) > 0 {
   104  			err = cfgValue.SetConfig(cfgMap)
   105  		} else if cfgSlice := parent.Slice(key); isCfgVal && len(cfgSlice) > 0 {
   106  			err = cfgValue.SetConfig(cfgSlice)
   107  		} else if v := parent.String(key); v != "" {
   108  			// otherwise parse via string (if-set)
   109  			err = f.Value.Set(v)
   110  		}
   111  	})
   112  }
   113  
   114  func readConfig() (Map, error) {
   115  	path, err := file.NormalizePath(configFilePath)
   116  	if err != nil {
   117  		return nil, fmt.Errorf("failed normalizing config file path %q: %v", configFilePath, err)
   118  	}
   119  	bytes, err := os.ReadFile(path)
   120  	if err != nil {
   121  		return nil, fmt.Errorf("failed reading %s: %v", path, err)
   122  	}
   123  	cfg := Map{}
   124  	if err := yaml.Unmarshal(bytes, cfg); err != nil {
   125  		return nil, fmt.Errorf("failed unmarshalling %s: %v", path, err)
   126  	}
   127  	return cfg, nil
   128  }