Non puoi selezionare più di 25 argomenti Gli argomenti devono iniziare con una lettera o un numero, possono includere trattini ('-') e possono essere lunghi fino a 35 caratteri.
 
 
 

419 righe
11 KiB

  1. package helpers
  2. import (
  3. "errors"
  4. "fmt"
  5. "io"
  6. "net/http"
  7. "os"
  8. "os/exec"
  9. "path/filepath"
  10. "runtime"
  11. "strings"
  12. "time"
  13. )
  14. const (
  15. NodeTypeMaster = "master"
  16. NodeTypeSlave = "slave"
  17. DefaultCNPort = 3100
  18. DefaultOVPort = 3101
  19. DefaultCNDBFileName = "cn.db"
  20. DefaultOVDBFileName = "ov.db"
  21. DefaultCNLogFileName = "cn.log"
  22. DefaultOVLogFileName = "ov.log"
  23. DefaultCNSessionName = "cn_session"
  24. DefaultOVSessionName = "ov_session"
  25. DefaultRootUsername = "root"
  26. defaultNodeReadyTimout = 45 * time.Second
  27. )
  28. // The e2e cluster runs on loopback only; each secret below resolves from the
  29. // environment first and falls back to a local-only value shared with the
  30. // Playwright fixtures (see web/e2e/fixtures/cluster.ts).
  31. func RootPassword() string {
  32. if password := strings.TrimSpace(os.Getenv("E2E_ROOT_PASSWORD")); password != "" {
  33. return password
  34. }
  35. return "e2e-local-root-pass"
  36. }
  37. func SessionSecret() string {
  38. if secret := strings.TrimSpace(os.Getenv("E2E_SESSION_SECRET")); secret != "" {
  39. return secret
  40. }
  41. return "e2e-local-session-secret"
  42. }
  43. func SyncAPIKey() string {
  44. if key := strings.TrimSpace(os.Getenv("E2E_SYNC_API_KEY")); key != "" {
  45. return key
  46. }
  47. return "e2e-local-sync-key"
  48. }
  49. type NodeProcess struct {
  50. Name string
  51. Port int
  52. BaseURL string
  53. DBPath string
  54. SessionName string
  55. NodeType string
  56. LogPath string
  57. cmd *exec.Cmd
  58. logFile *os.File
  59. waitCh chan error
  60. }
  61. type Cluster struct {
  62. RepoRoot string
  63. RunDir string
  64. BinaryPath string
  65. CNDBPath string
  66. OVDBPath string
  67. CN *NodeProcess
  68. OV *NodeProcess
  69. }
  70. type Environment struct {
  71. RepoRoot string
  72. ArtifactRoot string
  73. RunDir string
  74. BinaryPath string
  75. CNDBPath string
  76. OVDBPath string
  77. Cluster *Cluster
  78. CNClient *APIClient
  79. OVClient *APIClient
  80. }
  81. func BuildBinary(repoRoot, artifactRoot string) (string, error) {
  82. binDir := filepath.Join(artifactRoot, "bin")
  83. if err := os.MkdirAll(binDir, 0o755); err != nil {
  84. return "", fmt.Errorf("create binary dir: %w", err)
  85. }
  86. binaryName := "new-api-e2e"
  87. if runtime.GOOS == "windows" {
  88. binaryName += ".exe"
  89. }
  90. binaryPath := filepath.Join(binDir, binaryName)
  91. cmd := exec.Command("go", "build", "-o", binaryPath, ".")
  92. cmd.Dir = repoRoot
  93. output, err := cmd.CombinedOutput()
  94. if err != nil {
  95. return "", fmt.Errorf("build e2e binary: %w\n%s", err, string(output))
  96. }
  97. return binaryPath, nil
  98. }
  99. func PrepareEnvironment(repoRoot, artifactRoot, binaryPath, scenarioName string) (*Environment, error) {
  100. runDir := filepath.Join(artifactRoot, "runs", sanitizeName(scenarioName))
  101. if err := os.RemoveAll(runDir); err != nil {
  102. return nil, fmt.Errorf("reset run dir: %w", err)
  103. }
  104. if err := os.MkdirAll(runDir, 0o755); err != nil {
  105. return nil, fmt.Errorf("create run dir: %w", err)
  106. }
  107. cluster := NewCluster(repoRoot, runDir, binaryPath)
  108. if err := cluster.Start(NodeTypeMaster, NodeTypeMaster); err != nil {
  109. _ = cluster.Stop()
  110. return nil, err
  111. }
  112. cnClient, err := NewAPIClient(cluster.CN.BaseURL)
  113. if err != nil {
  114. _ = cluster.Stop()
  115. return nil, err
  116. }
  117. ovClient, err := NewAPIClient(cluster.OV.BaseURL)
  118. if err != nil {
  119. _ = cluster.Stop()
  120. return nil, err
  121. }
  122. if err := SetupRoot(cnClient, DefaultRootUsername, RootPassword()); err != nil {
  123. _ = cluster.Stop()
  124. return nil, fmt.Errorf("setup CN root: %w", err)
  125. }
  126. if err := SetupRoot(ovClient, DefaultRootUsername, RootPassword()); err != nil {
  127. _ = cluster.Stop()
  128. return nil, fmt.Errorf("setup OV root: %w", err)
  129. }
  130. if err := cluster.Stop(); err != nil {
  131. return nil, fmt.Errorf("stop bootstrap cluster: %w", err)
  132. }
  133. if err := SeedCNData(cluster.CNDBPath); err != nil {
  134. return nil, fmt.Errorf("seed CN data: %w", err)
  135. }
  136. if err := SeedOVData(cluster.OVDBPath); err != nil {
  137. return nil, fmt.Errorf("seed OV data: %w", err)
  138. }
  139. if err := cluster.Start(NodeTypeMaster, NodeTypeSlave); err != nil {
  140. _ = cluster.Stop()
  141. return nil, err
  142. }
  143. cnClient, err = NewAPIClient(cluster.CN.BaseURL)
  144. if err != nil {
  145. _ = cluster.Stop()
  146. return nil, err
  147. }
  148. ovClient, err = NewAPIClient(cluster.OV.BaseURL)
  149. if err != nil {
  150. _ = cluster.Stop()
  151. return nil, err
  152. }
  153. if err := Login(cnClient, DefaultRootUsername, RootPassword()); err != nil {
  154. _ = cluster.Stop()
  155. return nil, fmt.Errorf("login CN root: %w", err)
  156. }
  157. if err := Login(ovClient, DefaultRootUsername, RootPassword()); err != nil {
  158. _ = cluster.Stop()
  159. return nil, fmt.Errorf("login OV root: %w", err)
  160. }
  161. if err := ConfigureCNRegionSync(cnClient, cluster.CN.BaseURL, cluster.OV.BaseURL, SyncAPIKey()); err != nil {
  162. _ = cluster.Stop()
  163. return nil, fmt.Errorf("configure CN region sync: %w", err)
  164. }
  165. if err := ConfigureOVRegionSync(ovClient, cluster.CN.BaseURL, SyncAPIKey()); err != nil {
  166. _ = cluster.Stop()
  167. return nil, fmt.Errorf("configure OV region sync: %w", err)
  168. }
  169. if err := WaitOptionApplied(cluster.OV.BaseURL, SyncAPIKey(), 5, 20*time.Second); err != nil {
  170. _ = cluster.Stop()
  171. return nil, fmt.Errorf("wait OV migration API ready: %w", err)
  172. }
  173. return &Environment{
  174. RepoRoot: repoRoot,
  175. ArtifactRoot: artifactRoot,
  176. RunDir: runDir,
  177. BinaryPath: binaryPath,
  178. CNDBPath: cluster.CNDBPath,
  179. OVDBPath: cluster.OVDBPath,
  180. Cluster: cluster,
  181. CNClient: cnClient,
  182. OVClient: ovClient,
  183. }, nil
  184. }
  185. func (e *Environment) Close() error {
  186. if e == nil || e.Cluster == nil {
  187. return nil
  188. }
  189. return e.Cluster.Stop()
  190. }
  191. func NewCluster(repoRoot, runDir, binaryPath string) *Cluster {
  192. return &Cluster{
  193. RepoRoot: repoRoot,
  194. RunDir: runDir,
  195. BinaryPath: binaryPath,
  196. CNDBPath: filepath.Join(runDir, DefaultCNDBFileName),
  197. OVDBPath: filepath.Join(runDir, DefaultOVDBFileName),
  198. }
  199. }
  200. func (c *Cluster) Start(cnNodeType, ovNodeType string) error {
  201. if err := os.MkdirAll(c.RunDir, 0o755); err != nil {
  202. return fmt.Errorf("create run dir: %w", err)
  203. }
  204. cn, err := c.startNode("cn", DefaultCNPort, c.CNDBPath, DefaultCNSessionName, cnNodeType, DefaultCNLogFileName)
  205. if err != nil {
  206. return err
  207. }
  208. c.CN = cn
  209. ov, err := c.startNode("ov", DefaultOVPort, c.OVDBPath, DefaultOVSessionName, ovNodeType, DefaultOVLogFileName)
  210. if err != nil {
  211. _ = c.CN.Stop()
  212. c.CN = nil
  213. return err
  214. }
  215. c.OV = ov
  216. if err := c.CN.WaitReady(defaultNodeReadyTimout); err != nil {
  217. _ = c.Stop()
  218. return err
  219. }
  220. if err := c.OV.WaitReady(defaultNodeReadyTimout); err != nil {
  221. _ = c.Stop()
  222. return err
  223. }
  224. return nil
  225. }
  226. func (c *Cluster) Stop() error {
  227. var errs []string
  228. if c.OV != nil {
  229. if err := c.OV.Stop(); err != nil {
  230. errs = append(errs, err.Error())
  231. }
  232. c.OV = nil
  233. }
  234. if c.CN != nil {
  235. if err := c.CN.Stop(); err != nil {
  236. errs = append(errs, err.Error())
  237. }
  238. c.CN = nil
  239. }
  240. if len(errs) == 0 {
  241. return nil
  242. }
  243. return errors.New(strings.Join(errs, "; "))
  244. }
  245. func (c *Cluster) startNode(name string, port int, dbPath, sessionName, nodeType, logFileName string) (*NodeProcess, error) {
  246. logPath := filepath.Join(c.RunDir, logFileName)
  247. logFile, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o644)
  248. if err != nil {
  249. return nil, fmt.Errorf("open %s log file: %w", name, err)
  250. }
  251. systemLogDir := filepath.Join(c.RunDir, "system-logs", name)
  252. if err := os.MkdirAll(systemLogDir, 0o755); err != nil {
  253. _ = logFile.Close()
  254. return nil, fmt.Errorf("create %s system log dir: %w", name, err)
  255. }
  256. cmd := exec.Command(c.BinaryPath, "--log-dir", systemLogDir)
  257. cmd.Dir = c.RepoRoot
  258. cmd.Env = mergeEnv(os.Environ(), map[string]string{
  259. "PORT": fmt.Sprintf("%d", port),
  260. "SQLITE_PATH": dbPath,
  261. "SESSION_SECRET": SessionSecret(),
  262. "SESSION_NAME": sessionName,
  263. "SESSION_SECURE": "false",
  264. "SESSION_SAMESITE": "strict",
  265. "GLOBAL_API_RATE_LIMIT_ENABLE": "false",
  266. "GLOBAL_WEB_RATE_LIMIT_ENABLE": "false",
  267. "CRITICAL_RATE_LIMIT_ENABLE": "false",
  268. "GIN_MODE": "release",
  269. "NODE_TYPE": nodeType,
  270. })
  271. cmd.Stdout = logFile
  272. cmd.Stderr = logFile
  273. waitCh := make(chan error, 1)
  274. if err := cmd.Start(); err != nil {
  275. _ = logFile.Close()
  276. return nil, fmt.Errorf("start %s node: %w", name, err)
  277. }
  278. go func() {
  279. waitCh <- cmd.Wait()
  280. }()
  281. return &NodeProcess{
  282. Name: name,
  283. Port: port,
  284. BaseURL: fmt.Sprintf("http://127.0.0.1:%d", port),
  285. DBPath: dbPath,
  286. SessionName: sessionName,
  287. NodeType: nodeType,
  288. LogPath: logPath,
  289. cmd: cmd,
  290. logFile: logFile,
  291. waitCh: waitCh,
  292. }, nil
  293. }
  294. func (n *NodeProcess) WaitReady(timeout time.Duration) error {
  295. client := &http.Client{Timeout: 1 * time.Second}
  296. deadline := time.Now().Add(timeout)
  297. for time.Now().Before(deadline) {
  298. select {
  299. case err := <-n.waitCh:
  300. if err == nil {
  301. return fmt.Errorf("%s node exited before ready; see %s", n.Name, n.LogPath)
  302. }
  303. return fmt.Errorf("%s node exited before ready: %w; see %s", n.Name, err, n.LogPath)
  304. default:
  305. }
  306. resp, err := client.Get(n.BaseURL + "/api/status")
  307. if err == nil {
  308. _, _ = io.Copy(io.Discard, resp.Body)
  309. resp.Body.Close()
  310. if resp.StatusCode == http.StatusOK {
  311. return nil
  312. }
  313. }
  314. time.Sleep(250 * time.Millisecond)
  315. }
  316. return fmt.Errorf("%s node did not become ready within %s; see %s", n.Name, timeout, n.LogPath)
  317. }
  318. func (n *NodeProcess) Stop() error {
  319. if n == nil {
  320. return nil
  321. }
  322. var errs []string
  323. killRequested := false
  324. if n.cmd != nil && n.cmd.Process != nil {
  325. if err := n.cmd.Process.Kill(); err != nil && !errors.Is(err, os.ErrProcessDone) {
  326. errs = append(errs, fmt.Sprintf("kill %s node: %v", n.Name, err))
  327. } else {
  328. killRequested = true
  329. }
  330. select {
  331. case err := <-n.waitCh:
  332. if err != nil && !killRequested && !strings.Contains(strings.ToLower(err.Error()), "signal: killed") {
  333. errs = append(errs, fmt.Sprintf("wait %s node: %v", n.Name, err))
  334. }
  335. case <-time.After(5 * time.Second):
  336. errs = append(errs, fmt.Sprintf("wait %s node timeout", n.Name))
  337. }
  338. }
  339. if n.logFile != nil {
  340. if err := n.logFile.Close(); err != nil {
  341. errs = append(errs, fmt.Sprintf("close %s log: %v", n.Name, err))
  342. }
  343. n.logFile = nil
  344. }
  345. n.cmd = nil
  346. if len(errs) == 0 {
  347. return nil
  348. }
  349. return errors.New(strings.Join(errs, "; "))
  350. }
  351. func sanitizeName(name string) string {
  352. replacer := strings.NewReplacer("/", "_", "\\", "_", ":", "_", " ", "_")
  353. return replacer.Replace(name)
  354. }
  355. func mergeEnv(base []string, overrides map[string]string) []string {
  356. filtered := make([]string, 0, len(base)+len(overrides))
  357. for _, entry := range base {
  358. parts := strings.SplitN(entry, "=", 2)
  359. if len(parts) != 2 {
  360. continue
  361. }
  362. if _, ok := overrides[parts[0]]; ok {
  363. continue
  364. }
  365. filtered = append(filtered, entry)
  366. }
  367. for key, value := range overrides {
  368. filtered = append(filtered, key+"="+value)
  369. }
  370. return filtered
  371. }