Support retry strategy to vefify (#21)

diff --git a/commands/verify/verify.go b/commands/verify/verify.go
index 972769a..3307f43 100644
--- a/commands/verify/verify.go
+++ b/commands/verify/verify.go
@@ -19,6 +19,7 @@
 
 import (
 	"fmt"
+	"time"
 
 	"github.com/apache/skywalking-infra-e2e/internal/components/verifier"
 	"github.com/apache/skywalking-infra-e2e/internal/config"
@@ -91,14 +92,34 @@
 
 	e2eConfig := config.GlobalConfig.E2EConfig
 
-	for _, v := range e2eConfig.Verify {
-		if v.GetExpected() != "" {
-			if err := verifySingleCase(v.GetExpected(), v.GetActual(), v.Query); err != nil {
-				return err
+	retryCount := e2eConfig.Verify.RetryStrategy.Count
+	if retryCount <= 0 {
+		retryCount = 1
+	}
+	retryInterval := e2eConfig.Verify.RetryStrategy.Interval
+	if retryInterval < 0 {
+		retryInterval = 1000
+	}
+
+	var err error
+	for current := 1; current <= retryCount; current++ {
+		for _, v := range e2eConfig.Verify.Cases {
+			if v.GetExpected() != "" {
+				if err = verifySingleCase(v.GetExpected(), v.GetActual(), v.Query); err != nil {
+					break
+				}
+			} else {
+				return fmt.Errorf("the expected data file is not specified")
 			}
-		} else {
-			logger.Log.Error("the expected data file is not specified")
+		}
+
+		if err != nil && current != retryCount {
+			logger.Log.Warnf("verify case failure, will continue retry, %v", err)
+			time.Sleep(time.Duration(retryInterval) * time.Millisecond)
+		} else if err == nil {
+			return nil
 		}
 	}
-	return nil
+
+	return err
 }
diff --git a/examples/compose/e2e.yaml b/examples/compose/e2e.yaml
index b676518..459a586 100644
--- a/examples/compose/e2e.yaml
+++ b/examples/compose/e2e.yaml
@@ -32,9 +32,16 @@
   method: GET
 
 verify:
-  - actual: ../../test/verify/1.actual.yaml
-    expected: ../../test/verify/1.expected.yaml
-  - actual: ../../test/verify/2.actual.yaml
-    expected: ../../test/verify/2.expected.yaml
-  - query: swctl --display yaml --base-url=http://127.0.0.1:${oap_12800}/graphql service ls
-    expected: ../../test/verify/3.expected.yaml
\ No newline at end of file
+  # verify with retry strategy
+  retry:
+    # max retry count
+    count: 10
+    # the interval between two retries, in millisecond.
+    interval: 10000
+  cases:
+    - actual: ../../test/verify/1.actual.yaml
+      expected: ../../test/verify/1.expected.yaml
+    - actual: ../../test/verify/2.actual.yaml
+      expected: ../../test/verify/2.expected.yaml
+    - query: swctl --display yaml --base-url=http://127.0.0.1:${oap_12800}/graphql service ls
+      expected: ../../test/verify/3.expected.yaml
\ No newline at end of file
diff --git a/examples/kind/e2e.yaml b/examples/kind/e2e.yaml
index 7dc69f1..107ffea 100644
--- a/examples/kind/e2e.yaml
+++ b/examples/kind/e2e.yaml
@@ -56,11 +56,18 @@
   method: GET
 
 verify:
-  - actual: ../../test/verify/1.actual.yaml
-    expected: ../../test/verify/1.expected.yaml
-  - actual: ../../test/verify/2.actual.yaml
-    expected: ../../test/verify/2.expected.yaml
-  - actual: ../../test/verify/1.actual.yaml
-    expected: ../../test/verify/2.expected.yaml
-  - query: swctl --display yaml service ls
-    expected: ../../test/verify/3.expected.yaml
+  # verify with retry strategy
+  retry:
+    # max retry count
+    count: 10
+    # the interval between two retries, in millisecond.
+    interval: 10000
+  cases:
+    - actual: ../../test/verify/1.actual.yaml
+      expected: ../../test/verify/1.expected.yaml
+    - actual: ../../test/verify/2.actual.yaml
+      expected: ../../test/verify/2.expected.yaml
+    - actual: ../../test/verify/1.actual.yaml
+      expected: ../../test/verify/2.expected.yaml
+    - query: swctl --display yaml service ls
+      expected: ../../test/verify/3.expected.yaml
diff --git a/internal/config/e2eConfig.go b/internal/config/e2eConfig.go
index dfbc617..8554c9d 100644
--- a/internal/config/e2eConfig.go
+++ b/internal/config/e2eConfig.go
@@ -22,10 +22,10 @@
 
 // E2EConfig corresponds to configuration file e2e.yaml.
 type E2EConfig struct {
-	Setup   Setup        `yaml:"setup"`
-	Cleanup Cleanup      `yaml:"cleanup"`
-	Trigger Trigger      `yaml:"trigger"`
-	Verify  []VerifyCase `yaml:"verify"`
+	Setup   Setup   `yaml:"setup"`
+	Cleanup Cleanup `yaml:"cleanup"`
+	Trigger Trigger `yaml:"trigger"`
+	Verify  Verify  `yaml:"verify"`
 }
 
 type Setup struct {
@@ -46,6 +46,11 @@
 	Waits   []Wait `yaml:"wait"`
 }
 
+type Verify struct {
+	RetryStrategy VerifyRetryStrategy `yaml:"retry"`
+	Cases         []VerifyCase        `yaml:"cases"`
+}
+
 func (s *Setup) GetFile() string {
 	return util.ResolveAbs(s.File)
 }
@@ -81,6 +86,11 @@
 	Expected string `yaml:"expected"`
 }
 
+type VerifyRetryStrategy struct {
+	Count    int `yaml:"count"`
+	Interval int `yaml:"interval"`
+}
+
 // GetActual resolves the absolute file path of the actual data file.
 func (v *VerifyCase) GetActual() string {
 	return util.ResolveAbs(v.Actual)