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  }