istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pkg/test/framework/components/environment/kube/flags.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 kube
    16  
    17  import (
    18  	"flag"
    19  	"fmt"
    20  	"os"
    21  	"path/filepath"
    22  	"strconv"
    23  	"strings"
    24  
    25  	"gopkg.in/yaml.v3"
    26  
    27  	"istio.io/istio/pkg/test/env"
    28  	"istio.io/istio/pkg/test/framework/components/cluster"
    29  	"istio.io/istio/pkg/test/framework/config"
    30  	"istio.io/istio/pkg/test/scopes"
    31  	"istio.io/istio/pkg/test/util/file"
    32  )
    33  
    34  const (
    35  	defaultKubeConfig = "~/.kube/config"
    36  )
    37  
    38  const (
    39  	ArchAMD64 = "amd64"
    40  	ArchARM64 = "arm64"
    41  )
    42  
    43  var (
    44  	// Settings we will collect from the command-line.
    45  	settingsFromCommandLine = &Settings{
    46  		LoadBalancerSupported: true,
    47  		Architecture:          ArchAMD64,
    48  	}
    49  	// hold kubeconfigs from command line to split later
    50  	kubeConfigs string
    51  	// hold controlPlaneTopology from command line to parse later
    52  	controlPlaneTopology string
    53  	// hold networkTopology from command line to parse later
    54  	networkTopology string
    55  	// hold configTopology from command line to parse later
    56  	configTopology string
    57  	// file defining all types of topology
    58  	clusterConfigs configsVal
    59  )
    60  
    61  // NewSettingsFromCommandLine returns Settings obtained from command-line flags.
    62  // config.Parse must be called before calling this function.
    63  func NewSettingsFromCommandLine() (*Settings, error) {
    64  	if !config.Parsed() {
    65  		panic("config.Parse must be called before this function")
    66  	}
    67  
    68  	s := settingsFromCommandLine.clone()
    69  
    70  	// Process the kube clusterConfigs.
    71  	var err error
    72  	s.KubeConfig, err = parseKubeConfigs(kubeConfigs, ",")
    73  	if err != nil {
    74  		return nil, fmt.Errorf("error parsing KubeConfigs from command-line: %v", err)
    75  	}
    76  
    77  	s.controlPlaneTopology, err = newControlPlaneTopology()
    78  	if err != nil {
    79  		return nil, err
    80  	}
    81  
    82  	s.networkTopology, err = parseNetworkTopology()
    83  	if err != nil {
    84  		return nil, err
    85  	}
    86  
    87  	s.configTopology, err = newConfigTopology()
    88  	if err != nil {
    89  		return nil, err
    90  	}
    91  
    92  	return s, nil
    93  }
    94  
    95  func getKubeConfigsFromEnvironment() ([]string, error) {
    96  	// Normalize KUBECONFIG so that it is separated by the OS path list separator.
    97  	// The framework currently supports comma as a separator, but that violates the
    98  	// KUBECONFIG spec.
    99  	value := env.KUBECONFIG.Value()
   100  	if strings.Contains(value, ",") {
   101  		updatedValue := strings.ReplaceAll(value, ",", string(filepath.ListSeparator))
   102  		_ = os.Setenv(env.KUBECONFIG.Name(), updatedValue)
   103  		scopes.Framework.Warnf("KUBECONFIG contains commas: %s.\nReplacing with %s: %s", value,
   104  			string(filepath.ListSeparator), updatedValue)
   105  		value = updatedValue
   106  	}
   107  	out, err := parseKubeConfigs(value, string(filepath.ListSeparator))
   108  	if err != nil {
   109  		return nil, err
   110  	}
   111  	if len(out) == 0 {
   112  		scopes.Framework.Info("Environment variable KUBECONFIG unspecified, defaulting to ~/.kube/config.")
   113  		normalizedDefaultKubeConfig, err := file.NormalizePath(defaultKubeConfig)
   114  		if err != nil {
   115  			return nil, fmt.Errorf("error normalizing default kube config file %s: %v",
   116  				defaultKubeConfig, err)
   117  		}
   118  		out = []string{normalizedDefaultKubeConfig}
   119  	}
   120  	return out, nil
   121  }
   122  
   123  func parseKubeConfigs(value, separator string) ([]string, error) {
   124  	if len(value) == 0 {
   125  		return make([]string, 0), nil
   126  	}
   127  
   128  	parts := strings.Split(value, separator)
   129  	out := make([]string, 0, len(parts))
   130  	for _, f := range parts {
   131  		f := strings.TrimSpace(f)
   132  		if len(f) != 0 {
   133  			var err error
   134  			if f, err = file.NormalizePath(f); err != nil {
   135  				return nil, err
   136  			}
   137  			out = append(out, f)
   138  		}
   139  	}
   140  	return out, nil
   141  }
   142  
   143  func newControlPlaneTopology() (clusterTopology, error) {
   144  	topology, err := parseClusterTopology(controlPlaneTopology)
   145  	if err != nil {
   146  		return nil, err
   147  	}
   148  	if len(topology) == 0 {
   149  		return nil, nil
   150  	}
   151  	return topology, nil
   152  }
   153  
   154  func newConfigTopology() (clusterTopology, error) {
   155  	topology, err := parseClusterTopology(configTopology)
   156  	if err != nil {
   157  		return nil, err
   158  	}
   159  	if len(topology) == 0 {
   160  		return nil, nil
   161  	}
   162  	return topology, nil
   163  }
   164  
   165  func parseClusterTopology(topology string) (clusterTopology, error) {
   166  	if topology == "" {
   167  		return nil, nil
   168  	}
   169  	out := make(clusterTopology)
   170  
   171  	values := strings.Split(topology, ",")
   172  	for _, v := range values {
   173  		parts := strings.Split(v, ":")
   174  		if len(parts) != 2 {
   175  			return nil, fmt.Errorf("failed parsing topology mapping entry %s", v)
   176  		}
   177  		sourceCluster, err := parseClusterIndex(parts[0])
   178  		if err != nil {
   179  			return nil, err
   180  		}
   181  		targetCluster, err := parseClusterIndex(parts[1])
   182  		if err != nil {
   183  			return nil, err
   184  		}
   185  		if _, ok := out[sourceCluster]; ok {
   186  			return nil, fmt.Errorf("multiple mappings for source cluster %d", sourceCluster)
   187  		}
   188  		out[sourceCluster] = targetCluster
   189  	}
   190  	return out, nil
   191  }
   192  
   193  func parseNetworkTopology() (map[clusterIndex]string, error) {
   194  	if networkTopology == "" {
   195  		return nil, nil
   196  	}
   197  	out := make(map[clusterIndex]string)
   198  	values := strings.Split(networkTopology, ",")
   199  	for _, v := range values {
   200  		parts := strings.Split(v, ":")
   201  		if len(parts) != 2 {
   202  			return nil, fmt.Errorf("failed parsing network mapping entry %s", v)
   203  		}
   204  		cluster, err := parseClusterIndex(parts[0])
   205  		if err != nil {
   206  			return nil, err
   207  		}
   208  		if len(parts[1]) == 0 {
   209  			return nil, fmt.Errorf("failed parsing network mapping entry %s: failed parsing network name", v)
   210  		}
   211  		out[cluster] = parts[1]
   212  	}
   213  	return out, nil
   214  }
   215  
   216  func parseClusterIndex(index string) (clusterIndex, error) {
   217  	ci, err := strconv.Atoi(index)
   218  	if err != nil || ci < 0 {
   219  		return 0, fmt.Errorf("failed parsing cluster index: %s", index)
   220  	}
   221  	return clusterIndex(ci), nil
   222  }
   223  
   224  // configsVal implements config.Value to allow setting the path as a flag or embedding the topology content
   225  // in the overall test framework config
   226  type configsVal []cluster.Config
   227  
   228  func (c *configsVal) String() string {
   229  	return fmt.Sprint(*c)
   230  }
   231  
   232  func (c *configsVal) Set(s string) error {
   233  	filename, err := file.NormalizePath(s)
   234  	if err != nil {
   235  		return err
   236  	}
   237  	topologyBytes, err := os.ReadFile(filename)
   238  	if err != nil {
   239  		return err
   240  	}
   241  	configs := []cluster.Config{}
   242  	if err := yaml.Unmarshal(topologyBytes, &configs); err != nil {
   243  		return fmt.Errorf("failed to parse %s: %v", s, err)
   244  	}
   245  	*c = configs
   246  	scopes.Framework.Infof("Using clusterConfigs file: %v.", s)
   247  	return nil
   248  }
   249  
   250  func (c *configsVal) SetConfig(m any) error {
   251  	bytes, err := yaml.Marshal(m)
   252  	if err != nil {
   253  		return err
   254  	}
   255  	configs := []cluster.Config{}
   256  	if err := yaml.Unmarshal(bytes, &configs); err != nil {
   257  		return fmt.Errorf("failed to reparse: %v", err)
   258  	}
   259  	*c = configs
   260  	scopes.Framework.Infof("Using topology from test framework config file.")
   261  	return nil
   262  }
   263  
   264  var _ config.Value = &configsVal{}
   265  
   266  // init registers the command-line flags that we can exposed for "go test".
   267  func init() {
   268  	flag.StringVar(&kubeConfigs, "istio.test.kube.config", "",
   269  		"A comma-separated list of paths to kube config files for cluster environments.")
   270  	flag.BoolVar(&settingsFromCommandLine.LoadBalancerSupported, "istio.test.kube.loadbalancer", settingsFromCommandLine.LoadBalancerSupported,
   271  		"Indicates whether or not clusters in the environment support external IPs for LoadBalaner services. Used "+
   272  			"to obtain the right IP address for the Ingress Gateway. Set --istio.test.kube.loadbalancer=false for local KinD tests."+
   273  			"without MetalLB installed.")
   274  	flag.StringVar(&settingsFromCommandLine.Architecture, "istio.test.kube.architecture", settingsFromCommandLine.Architecture,
   275  		"Indicates the architecture (arm64 or amd64) of the cluster under test. This is used to customize tests that require per-arch specific settings")
   276  	flag.StringVar(&controlPlaneTopology, "istio.test.kube.controlPlaneTopology",
   277  		"", "Specifies the mapping for each cluster to the cluster hosting its control plane. The value is a "+
   278  			"comma-separated list of the form <clusterIndex>:<controlPlaneClusterIndex>, where the indexes refer to the order in which "+
   279  			"a given cluster appears in the 'istio.test.kube.config' flag. This topology also determines where control planes should "+
   280  			"be deployed. If not specified, the default is to deploy a control plane per cluster (i.e. `replicated control "+
   281  			"planes') and map every cluster to itself (e.g. 0:0,1:1,...).")
   282  	flag.StringVar(&networkTopology, "istio.test.kube.networkTopology",
   283  		"", "Specifies the mapping for each cluster to it's network name, for multi-network scenarios. The value is a "+
   284  			"comma-separated list of the form <clusterIndex>:<networkName>, where the indexes refer to the order in which "+
   285  			"a given cluster appears in the 'istio.test.kube.config' flag. If not specified, network name will be left unset")
   286  	flag.StringVar(&configTopology, "istio.test.kube.configTopology",
   287  		"", "Specifies the mapping for each cluster to the cluster hosting its config. The value is a "+
   288  			"comma-separated list of the form <clusterIndex>:<configClusterIndex>, where the indexes refer to the order in which "+
   289  			"a given cluster appears in the 'istio.test.kube.config' flag. If not specified, the default is every cluster maps to itself(e.g. 0:0,1:1,...).")
   290  	flag.Var(&clusterConfigs, "istio.test.kube.topology", "The path to a JSON file that defines control plane,"+
   291  		" network, and config cluster topology. The JSON document should be an array of objects that contain the keys \"control_plane_index\","+
   292  		" \"network_id\" and \"config_index\" with all integer values. If control_plane_index is omitted, the index of the array item is used."+
   293  		"If network_id is omitted, 0 will be used. If config_index is omitted, control_plane_index will be used.")
   294  	flag.BoolVar(&settingsFromCommandLine.MCSControllerEnabled, "istio.test.kube.mcs.controllerEnabled", settingsFromCommandLine.MCSControllerEnabled,
   295  		"Indicates whether the Kubernetes environment has a Multi-Cluster Services (MCS) controller running.")
   296  	flag.StringVar(&settingsFromCommandLine.MCSAPIGroup, "istio.test.kube.mcs.apiGroup", "multicluster.x-k8s.io",
   297  		"The group to be used for the Kubernetes Multi-Cluster Services (MCS) API.")
   298  	flag.StringVar(&settingsFromCommandLine.MCSAPIVersion, "istio.test.kube.mcs.apiVersion", "v1alpha1",
   299  		"The version to be used for the Kubernets Multi-Cluster Services (MCS) API.")
   300  }