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 }