github.com/NVIDIA/aistore@v1.3.23-0.20240517131212-7df6609be51d/tools/init.go (about) 1 // Package tools provides common tools and utilities for all unit and integration tests 2 /* 3 * Copyright (c) 2018-2024, NVIDIA CORPORATION. All rights reserved. 4 */ 5 package tools 6 7 import ( 8 "errors" 9 "fmt" 10 "io" 11 "net/http" 12 "net/url" 13 "os" 14 "strings" 15 "sync" 16 "time" 17 18 "github.com/NVIDIA/aistore/api" 19 "github.com/NVIDIA/aistore/api/authn" 20 "github.com/NVIDIA/aistore/api/env" 21 "github.com/NVIDIA/aistore/cmn" 22 "github.com/NVIDIA/aistore/cmn/cos" 23 "github.com/NVIDIA/aistore/core/meta" 24 "github.com/NVIDIA/aistore/tools/docker" 25 "github.com/NVIDIA/aistore/tools/tlog" 26 ) 27 28 const ( 29 defaultProxyURL = "http://localhost:8080" // the url for the cluster's proxy (local) 30 dockerEnvFile = "/tmp/docker_ais/deploy.env" // filepath of Docker deployment config 31 ) 32 33 const ( 34 registerTimeout = time.Minute * 2 35 ) 36 37 type ( 38 // command used to restore a node 39 RestoreCmd struct { 40 Node *meta.Snode 41 Cmd string 42 Args []string 43 PID int 44 } 45 ClusterType string 46 ) 47 48 // Cluster type used for test 49 const ( 50 ClusterTypeLocal ClusterType = "local" 51 ClusterTypeDocker ClusterType = "docker" 52 ClusterTypeK8s ClusterType = "k8s" 53 ) 54 55 type g struct { 56 Client *http.Client 57 Log func(format string, a ...any) 58 } 59 60 var ( 61 proxyURLReadOnly string // user-defined primary proxy URL - it is read-only variable and tests mustn't change it 62 pmapReadOnly meta.NodeMap // initial proxy map - it is read-only variable 63 testClusterType ClusterType // AIS cluster type - it is read-only variable 64 65 currSmap *meta.Smap 66 67 restoreNodesOnce sync.Once // Ensures that the initialization happens only once. 68 restoreNodes map[string]RestoreCmd // initial proxy and target nodes => command to restore them 69 70 transportArgs = cmn.TransportArgs{ 71 Timeout: 600 * time.Second, 72 UseHTTPProxyEnv: true, 73 74 // Allow a lot of idle connections so they can be reused when making huge 75 // number of requests (eg. in `TestETLBigBucket`). 76 MaxIdleConns: 2000, 77 IdleConnsPerHost: 200, 78 } 79 tlsArgs = cmn.TLSArgs{ 80 SkipVerify: true, 81 } 82 83 RemoteCluster struct { 84 UUID string 85 Alias string 86 URL string 87 } 88 LoggedUserToken string 89 90 gctx g 91 ) 92 93 // NOTE: 94 // With no access to cluster configuration the tests 95 // currently simply detect protocol type by the env.AIS.Endpoint (proxy's) URL. 96 // Certificate check and other TLS is always disabled. 97 98 func init() { 99 gctx.Log = tlog.Logf 100 101 if cos.IsHTTPS(os.Getenv(env.AIS.Endpoint)) { 102 // fill-in from env 103 cmn.EnvToTLS(&tlsArgs) 104 gctx.Client = cmn.NewClientTLS(transportArgs, tlsArgs) 105 } else { 106 gctx.Client = cmn.NewClient(transportArgs) 107 } 108 } 109 110 func NewClientWithProxy(proxyURL string) *http.Client { 111 var ( 112 transport = cmn.NewTransport(transportArgs) 113 parsedURL, err = url.Parse(proxyURL) 114 ) 115 cos.AssertNoErr(err) 116 transport.Proxy = http.ProxyURL(parsedURL) 117 118 if parsedURL.Scheme == "https" { 119 cos.AssertMsg(cos.IsHTTPS(proxyURL), proxyURL) 120 tlsConfig, err := cmn.NewTLS(tlsArgs) 121 cos.AssertNoErr(err) 122 transport.TLSClientConfig = tlsConfig 123 } 124 return &http.Client{ 125 Transport: transport, 126 Timeout: transportArgs.Timeout, 127 } 128 } 129 130 // InitLocalCluster initializes AIS cluster that must be either: 131 // 1. deployed locally using `make deploy` command and accessible @ localhost:8080, or 132 // 2. deployed in local docker environment, or 133 // 3. provided via `AIS_ENDPOINT` environment variable 134 // 135 // In addition, try to query remote AIS cluster that may or may not be locally deployed as well. 136 func InitLocalCluster() { 137 var ( 138 // Gets the fields from the .env file from which the docker was deployed 139 envVars = parseEnvVariables(dockerEnvFile) 140 // Host IP and port of primary cluster 141 primaryHostIP, port = envVars["PRIMARY_HOST_IP"], envVars["PORT"] 142 143 clusterType = ClusterTypeLocal 144 proxyURL = defaultProxyURL 145 ) 146 147 if docker.IsRunning() { 148 clusterType = ClusterTypeDocker 149 proxyURL = "http://" + primaryHostIP + ":" + port 150 } 151 152 // This is needed for testing on Kubernetes if we want to run 'make test-XXX' 153 // Many of the other packages do not accept the 'url' flag 154 if cliAISURL := os.Getenv(env.AIS.Endpoint); cliAISURL != "" { 155 if !strings.HasPrefix(cliAISURL, "http") { 156 cliAISURL = "http://" + cliAISURL 157 } 158 proxyURL = cliAISURL 159 } 160 161 err := InitCluster(proxyURL, clusterType) 162 if err == nil { 163 initRemAis() // remote AIS that optionally may be run locally as well and used for testing 164 return 165 } 166 fmt.Printf("Error: %s\n\n", strings.TrimSuffix(err.Error(), "\n")) 167 if strings.Contains(err.Error(), "token") { 168 fmt.Printf("Hint: make sure to provide access token via %s environment or the default config location\n", 169 env.AuthN.TokenFile) 170 } else if strings.Contains(err.Error(), "unreachable") { 171 fmt.Printf("Hint: make sure that cluster is running and/or specify its endpoint via %s environment\n", 172 env.AIS.Endpoint) 173 } else { 174 fmt.Printf("Hint: check api/env/*.go environment and, in particular %q\n", env.AIS.Endpoint) 175 if len(envVars) > 0 { 176 fmt.Println("Docker Environment:") 177 for k, v := range envVars { 178 fmt.Printf("\t%s:\t%s\n", k, v) 179 } 180 } 181 } 182 os.Exit(1) 183 } 184 185 // InitCluster initializes the environment necessary for testing against an AIS cluster. 186 // NOTE: the function is also used for testing by NVIDIA/ais-k8s Operator 187 func InitCluster(proxyURL string, clusterType ClusterType) (err error) { 188 LoggedUserToken = authn.LoadToken("") 189 proxyURLReadOnly = proxyURL 190 testClusterType = clusterType 191 if err = initProxyURL(); err != nil { 192 return 193 } 194 initPmap() 195 return 196 } 197 198 func initProxyURL() (err error) { 199 // Discover if a proxy is ready to accept requests. 200 err = cmn.NetworkCallWithRetry(&cmn.RetryArgs{ 201 Call: func() (int, error) { return 0, GetProxyReadiness(proxyURLReadOnly) }, 202 SoftErr: 5, 203 HardErr: 5, 204 Sleep: 5 * time.Second, 205 Action: "reach AIS at " + proxyURLReadOnly, 206 IsClient: true, 207 }) 208 if err != nil { 209 return errors.New("AIS is unreachable at " + proxyURLReadOnly) 210 } 211 212 if testClusterType == ClusterTypeK8s { 213 // For kubernetes cluster, we use LoadBalancer service to expose the proxies. 214 // `proxyURLReadOnly` will point to LoadBalancer service, and we need not get primary URL. 215 return 216 } 217 218 // Primary proxy can change if proxy tests are run and 219 // no new cluster is re-deployed before each test. 220 // Finds who is the current primary proxy. 221 primary, err := GetPrimaryProxy(proxyURLReadOnly) 222 if err != nil { 223 err = fmt.Errorf("failed to get primary proxy info from %s; err %v", proxyURLReadOnly, err) 224 return err 225 } 226 proxyURLReadOnly = primary.URL(cmn.NetPublic) 227 return 228 } 229 230 func initPmap() { 231 bp := BaseAPIParams(proxyURLReadOnly) 232 smap, err := waitForStartup(bp) 233 cos.AssertNoErr(err) 234 pmapReadOnly = smap.Pmap 235 } 236 237 func initRemAis() { 238 all, err := api.GetRemoteAIS(BaseAPIParams(proxyURLReadOnly)) 239 if err != nil { 240 if !errors.Is(err, io.EOF) { 241 fmt.Fprintf(os.Stderr, "failed to query remote ais cluster: %v\n", err) 242 } 243 return 244 } 245 cos.AssertMsg(len(all.A) < 2, "multi-remote clustering is not implemented yet") 246 if len(all.A) == 1 { 247 remais := all.A[0] 248 RemoteCluster.UUID = remais.UUID 249 RemoteCluster.Alias = remais.Alias 250 RemoteCluster.URL = remais.URL 251 } 252 } 253 254 func initNodeCmd() { 255 bp := BaseAPIParams(proxyURLReadOnly) 256 smap, err := waitForStartup(bp) 257 cos.AssertNoErr(err) 258 restoreNodes = make(map[string]RestoreCmd, smap.CountProxies()+smap.CountTargets()) 259 for _, node := range smap.Pmap { 260 if node.ID() == MockDaemonID { 261 continue 262 } 263 restoreNodes[node.ID()] = GetRestoreCmd(node) 264 } 265 266 for _, node := range smap.Tmap { 267 if node.ID() == MockDaemonID { 268 continue 269 } 270 restoreNodes[node.ID()] = GetRestoreCmd(node) 271 } 272 } 273 274 // reads .env file and parses its contents 275 func parseEnvVariables(fpath string, delimiter ...string) map[string]string { 276 m := map[string]string{} 277 dlim := "=" 278 data, err := os.ReadFile(fpath) 279 if err != nil { 280 return nil 281 } 282 283 if len(delimiter) > 0 { 284 dlim = delimiter[0] 285 } 286 287 paramList := strings.Split(string(data), "\n") 288 for _, dat := range paramList { 289 datum := strings.Split(dat, dlim) 290 // key=val 291 if len(datum) == 2 { 292 key := strings.TrimSpace(datum[0]) 293 value := strings.TrimSpace(datum[1]) 294 m[key] = value 295 } 296 } 297 return m 298 }