diff --git a/.github/workflows/pr_validation.yml b/.github/workflows/pr_validation.yml index 08b8388b5..7d9e74fe7 100644 --- a/.github/workflows/pr_validation.yml +++ b/.github/workflows/pr_validation.yml @@ -1,9 +1,15 @@ name: PR validation + on: pull_request: branches: - 'develop' - 'main' + +concurrency: + group: mizu-pr-validation-${{ github.ref }} + cancel-in-progress: true + jobs: build-cli: name: Build CLI @@ -38,43 +44,3 @@ jobs: - name: Build Agent run: make agent - - run-tests-cli: - name: Run CLI tests - runs-on: ubuntu-latest - steps: - - name: Set up Go 1.16 - uses: actions/setup-go@v2 - with: - go-version: '^1.16' - - - name: Check out code into the Go module directory - uses: actions/checkout@v2 - - - name: Test - run: make test-cli - - - name: Upload coverage to Codecov - uses: codecov/codecov-action@v2 - - run-tests-agent: - name: Run Agent tests - runs-on: ubuntu-latest - steps: - - name: Set up Go 1.16 - uses: actions/setup-go@v2 - with: - go-version: '^1.16' - - - name: Check out code into the Go module directory - uses: actions/checkout@v2 - - - shell: bash - run: | - sudo apt-get install libpcap-dev - - - name: Test - run: make test-agent - - - name: Upload coverage to Codecov - uses: codecov/codecov-action@v2 diff --git a/.github/workflows/tests_validation.yml b/.github/workflows/tests_validation.yml new file mode 100644 index 000000000..f63302bf7 --- /dev/null +++ b/.github/workflows/tests_validation.yml @@ -0,0 +1,56 @@ +name: tests validation + +on: + pull_request: + branches: + - 'develop' + - 'main' + push: + branches: + - 'develop' + - 'main' + +concurrency: + group: mizu-tests-validation-${{ github.ref }} + cancel-in-progress: true + +jobs: + run-tests-cli: + name: Run CLI tests + runs-on: ubuntu-latest + steps: + - name: Set up Go 1.16 + uses: actions/setup-go@v2 + with: + go-version: '^1.16' + + - name: Check out code into the Go module directory + uses: actions/checkout@v2 + + - name: Test + run: make test-cli + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v2 + + run-tests-agent: + name: Run Agent tests + runs-on: ubuntu-latest + steps: + - name: Set up Go 1.16 + uses: actions/setup-go@v2 + with: + go-version: '^1.16' + + - name: Check out code into the Go module directory + uses: actions/checkout@v2 + + - shell: bash + run: | + sudo apt-get install libpcap-dev + + - name: Test + run: make test-agent + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v2 diff --git a/acceptanceTests/Makefile b/acceptanceTests/Makefile index 8a142a160..c6d411544 100644 --- a/acceptanceTests/Makefile +++ b/acceptanceTests/Makefile @@ -1,2 +1,2 @@ test: ## Run acceptance tests. - @go test ./... + @go test ./... -timeout 1h diff --git a/acceptanceTests/config_test.go b/acceptanceTests/config_test.go new file mode 100644 index 000000000..248e56e9e --- /dev/null +++ b/acceptanceTests/config_test.go @@ -0,0 +1,283 @@ +package acceptanceTests + +import ( + "fmt" + "gopkg.in/yaml.v3" + "io/ioutil" + "os" + "os/exec" + "testing" +) + +type tapConfig struct { + GuiPort uint16 `yaml:"gui-port"` +} + +type configStruct struct { + Tap tapConfig `yaml:"tap"` +} + +func TestConfigRegenerate(t *testing.T) { + if testing.Short() { + t.Skip("ignored acceptance test") + } + + cliPath, cliPathErr := getCliPath() + if cliPathErr != nil { + t.Errorf("failed to get cli path, err: %v", cliPathErr) + return + } + + configPath, configPathErr := getConfigPath() + if configPathErr != nil { + t.Errorf("failed to get config path, err: %v", cliPathErr) + return + } + + configCmdArgs := getDefaultConfigCommandArgs() + + configCmdArgs = append(configCmdArgs, "-r") + + configCmd := exec.Command(cliPath, configCmdArgs...) + t.Logf("running command: %v", configCmd.String()) + + t.Cleanup(func() { + if err := os.Remove(configPath); err != nil { + t.Logf("failed to delete config file, err: %v", err) + } + }) + + if err := configCmd.Start(); err != nil { + t.Errorf("failed to start config command, err: %v", err) + return + } + + if err := configCmd.Wait(); err != nil { + t.Errorf("failed to wait config command, err: %v", err) + return + } + + _, readFileErr := ioutil.ReadFile(configPath) + if readFileErr != nil { + t.Errorf("failed to read config file, err: %v", readFileErr) + return + } +} + +func TestConfigGuiPort(t *testing.T) { + if testing.Short() { + t.Skip("ignored acceptance test") + } + + tests := []uint16{8898} + + for _, guiPort := range tests { + t.Run(fmt.Sprintf("%d", guiPort), func(t *testing.T) { + cliPath, cliPathErr := getCliPath() + if cliPathErr != nil { + t.Errorf("failed to get cli path, err: %v", cliPathErr) + return + } + + configPath, configPathErr := getConfigPath() + if configPathErr != nil { + t.Errorf("failed to get config path, err: %v", cliPathErr) + return + } + + config := configStruct{} + config.Tap.GuiPort = guiPort + + configBytes, marshalErr := yaml.Marshal(config) + if marshalErr != nil { + t.Errorf("failed to marshal config, err: %v", marshalErr) + return + } + + if writeErr := ioutil.WriteFile(configPath, configBytes, 0644); writeErr != nil { + t.Errorf("failed to write config to file, err: %v", writeErr) + return + } + + tapCmdArgs := getDefaultTapCommandArgs() + + tapNamespace := getDefaultTapNamespace() + tapCmdArgs = append(tapCmdArgs, tapNamespace...) + + tapCmd := exec.Command(cliPath, tapCmdArgs...) + t.Logf("running command: %v", tapCmd.String()) + + t.Cleanup(func() { + if err := cleanupCommand(tapCmd); err != nil { + t.Logf("failed to cleanup tap command, err: %v", err) + } + + if err := os.Remove(configPath); err != nil { + t.Logf("failed to delete config file, err: %v", err) + } + }) + + if err := tapCmd.Start(); err != nil { + t.Errorf("failed to start tap command, err: %v", err) + return + } + + apiServerUrl := getApiServerUrl(guiPort) + + if err := waitTapPodsReady(apiServerUrl); err != nil { + t.Errorf("failed to start tap pods on time, err: %v", err) + return + } + }) + } +} + +func TestConfigSetGuiPort(t *testing.T) { + if testing.Short() { + t.Skip("ignored acceptance test") + } + + tests := []struct { + ConfigFileGuiPort uint16 + SetGuiPort uint16 + }{ + {ConfigFileGuiPort: 8898, SetGuiPort: 8897}, + } + + for _, guiPortStruct := range tests { + t.Run(fmt.Sprintf("%d", guiPortStruct.SetGuiPort), func(t *testing.T) { + cliPath, cliPathErr := getCliPath() + if cliPathErr != nil { + t.Errorf("failed to get cli path, err: %v", cliPathErr) + return + } + + configPath, configPathErr := getConfigPath() + if configPathErr != nil { + t.Errorf("failed to get config path, err: %v", cliPathErr) + return + } + + config := configStruct{} + config.Tap.GuiPort = guiPortStruct.ConfigFileGuiPort + + configBytes, marshalErr := yaml.Marshal(config) + if marshalErr != nil { + t.Errorf("failed to marshal config, err: %v", marshalErr) + return + } + + if writeErr := ioutil.WriteFile(configPath, configBytes, 0644); writeErr != nil { + t.Errorf("failed to write config to file, err: %v", writeErr) + return + } + + tapCmdArgs := getDefaultTapCommandArgs() + + tapNamespace := getDefaultTapNamespace() + tapCmdArgs = append(tapCmdArgs, tapNamespace...) + + tapCmdArgs = append(tapCmdArgs, "--set", fmt.Sprintf("tap.gui-port=%v", guiPortStruct.SetGuiPort)) + + tapCmd := exec.Command(cliPath, tapCmdArgs...) + t.Logf("running command: %v", tapCmd.String()) + + t.Cleanup(func() { + if err := cleanupCommand(tapCmd); err != nil { + t.Logf("failed to cleanup tap command, err: %v", err) + } + + if err := os.Remove(configPath); err != nil { + t.Logf("failed to delete config file, err: %v", err) + } + }) + + if err := tapCmd.Start(); err != nil { + t.Errorf("failed to start tap command, err: %v", err) + return + } + + apiServerUrl := getApiServerUrl(guiPortStruct.SetGuiPort) + + if err := waitTapPodsReady(apiServerUrl); err != nil { + t.Errorf("failed to start tap pods on time, err: %v", err) + return + } + }) + } +} + +func TestConfigFlagGuiPort(t *testing.T) { + if testing.Short() { + t.Skip("ignored acceptance test") + } + + tests := []struct { + ConfigFileGuiPort uint16 + FlagGuiPort uint16 + }{ + {ConfigFileGuiPort: 8898, FlagGuiPort: 8896}, + } + + for _, guiPortStruct := range tests { + t.Run(fmt.Sprintf("%d", guiPortStruct.FlagGuiPort), func(t *testing.T) { + cliPath, cliPathErr := getCliPath() + if cliPathErr != nil { + t.Errorf("failed to get cli path, err: %v", cliPathErr) + return + } + + configPath, configPathErr := getConfigPath() + if configPathErr != nil { + t.Errorf("failed to get config path, err: %v", cliPathErr) + return + } + + config := configStruct{} + config.Tap.GuiPort = guiPortStruct.ConfigFileGuiPort + + configBytes, marshalErr := yaml.Marshal(config) + if marshalErr != nil { + t.Errorf("failed to marshal config, err: %v", marshalErr) + return + } + + if writeErr := ioutil.WriteFile(configPath, configBytes, 0644); writeErr != nil { + t.Errorf("failed to write config to file, err: %v", writeErr) + return + } + + tapCmdArgs := getDefaultTapCommandArgs() + + tapNamespace := getDefaultTapNamespace() + tapCmdArgs = append(tapCmdArgs, tapNamespace...) + + tapCmdArgs = append(tapCmdArgs, "-p", fmt.Sprintf("%v", guiPortStruct.FlagGuiPort)) + + tapCmd := exec.Command(cliPath, tapCmdArgs...) + t.Logf("running command: %v", tapCmd.String()) + + t.Cleanup(func() { + if err := cleanupCommand(tapCmd); err != nil { + t.Logf("failed to cleanup tap command, err: %v", err) + } + + if err := os.Remove(configPath); err != nil { + t.Logf("failed to delete config file, err: %v", err) + } + }) + + if err := tapCmd.Start(); err != nil { + t.Errorf("failed to start tap command, err: %v", err) + return + } + + apiServerUrl := getApiServerUrl(guiPortStruct.FlagGuiPort) + + if err := waitTapPodsReady(apiServerUrl); err != nil { + t.Errorf("failed to start tap pods on time, err: %v", err) + return + } + }) + } +} diff --git a/acceptanceTests/go.mod b/acceptanceTests/go.mod index bc529ac9d..071a91936 100644 --- a/acceptanceTests/go.mod +++ b/acceptanceTests/go.mod @@ -1,3 +1,5 @@ module github.com/up9inc/mizu/tests go 1.16 + +require gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b diff --git a/acceptanceTests/go.sum b/acceptanceTests/go.sum new file mode 100644 index 000000000..e387ff0b1 --- /dev/null +++ b/acceptanceTests/go.sum @@ -0,0 +1,4 @@ +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/acceptanceTests/setup.sh b/acceptanceTests/setup.sh index 0ed9dbce1..d95b72eea 100644 --- a/acceptanceTests/setup.sh +++ b/acceptanceTests/setup.sh @@ -26,14 +26,21 @@ fi echo "Starting minikube..." minikube start -echo "Creating mizu tests namespace" +echo "Creating mizu tests namespaces" kubectl create namespace mizu-tests +kubectl create namespace mizu-tests2 -echo "Creating httpbin deployment" +echo "Creating httpbin deployments" kubectl create deployment httpbin --image=kennethreitz/httpbin -n mizu-tests +kubectl create deployment httpbin2 --image=kennethreitz/httpbin -n mizu-tests -echo "Creating httpbin service" +kubectl create deployment httpbin --image=kennethreitz/httpbin -n mizu-tests2 + +echo "Creating httpbin services" kubectl expose deployment httpbin --type=NodePort --port=80 -n mizu-tests +kubectl expose deployment httpbin2 --type=NodePort --port=80 -n mizu-tests + +kubectl expose deployment httpbin --type=NodePort --port=80 -n mizu-tests2 echo "Starting proxy" kubectl proxy --port=8080 & diff --git a/acceptanceTests/tap_test.go b/acceptanceTests/tap_test.go index 1e6ca9a4f..8d921ed1f 100644 --- a/acceptanceTests/tap_test.go +++ b/acceptanceTests/tap_test.go @@ -1,9 +1,13 @@ package acceptanceTests import ( + "bytes" + "encoding/json" "fmt" "io/ioutil" + "net/http" "os/exec" + "strings" "testing" "time" ) @@ -24,6 +28,10 @@ func TestTapAndFetch(t *testing.T) { } tapCmdArgs := getDefaultTapCommandArgs() + + tapNamespace := getDefaultTapNamespace() + tapCmdArgs = append(tapCmdArgs, tapNamespace...) + tapCmd := exec.Command(cliPath, tapCmdArgs...) t.Logf("running command: %v", tapCmd.String()) @@ -38,14 +46,16 @@ func TestTapAndFetch(t *testing.T) { return } - if err := waitTapPodsReady(); err != nil { + apiServerUrl := getApiServerUrl(defaultApiServerPort) + + if err := waitTapPodsReady(apiServerUrl); err != nil { t.Errorf("failed to start tap pods on time, err: %v", err) return } - proxyUrl := "http://localhost:8080/api/v1/namespaces/mizu-tests/services/httpbin/proxy/get" + proxyUrl := getProxyUrl(defaultNamespaceName, defaultServiceName) for i := 0; i < entriesCount; i++ { - if _, requestErr := executeHttpRequest(proxyUrl); requestErr != nil { + if _, requestErr := executeHttpGetRequest(fmt.Sprintf("%v/get", proxyUrl)); requestErr != nil { t.Errorf("failed to send proxy request, err: %v", requestErr) return } @@ -54,28 +64,22 @@ func TestTapAndFetch(t *testing.T) { entriesCheckFunc := func() error { timestamp := time.Now().UnixNano() / int64(time.Millisecond) - entriesUrl := fmt.Sprintf("http://localhost:8899/mizu/api/entries?limit=%v&operator=lt×tamp=%v", entriesCount, timestamp) - requestResult, requestErr := executeHttpRequest(entriesUrl) + entriesUrl := fmt.Sprintf("%v/api/entries?limit=%v&operator=lt×tamp=%v", apiServerUrl, entriesCount, timestamp) + requestResult, requestErr := executeHttpGetRequest(entriesUrl) if requestErr != nil { return fmt.Errorf("failed to get entries, err: %v", requestErr) } - entries, ok := requestResult.([]interface{}) - if !ok { - return fmt.Errorf("invalid entries type") - } + entries := requestResult.([]interface{}) if len(entries) == 0 { return fmt.Errorf("unexpected entries result - Expected more than 0 entries") } - entry, ok := entries[0].(map[string]interface{}) - if !ok { - return fmt.Errorf("invalid entry type") - } + entry := entries[0].(map[string]interface{}) - entryUrl := fmt.Sprintf("http://localhost:8899/mizu/api/entries/%v", entry["id"]) - requestResult, requestErr = executeHttpRequest(entryUrl) + entryUrl := fmt.Sprintf("%v/api/entries/%v", apiServerUrl, entry["id"]) + requestResult, requestErr = executeHttpGetRequest(entryUrl) if requestErr != nil { return fmt.Errorf("failed to get entry, err: %v", requestErr) } @@ -86,7 +90,7 @@ func TestTapAndFetch(t *testing.T) { return nil } - if err := retriesExecute(ShortRetriesCount, entriesCheckFunc); err != nil { + if err := retriesExecute(shortRetriesCount, entriesCheckFunc); err != nil { t.Errorf("%v", err) return } @@ -117,10 +121,634 @@ func TestTapAndFetch(t *testing.T) { return nil } - if err := retriesExecute(ShortRetriesCount, harCheckFunc); err != nil { + if err := retriesExecute(shortRetriesCount, harCheckFunc); err != nil { t.Errorf("%v", err) return } }) } } + +func TestTapGuiPort(t *testing.T) { + if testing.Short() { + t.Skip("ignored acceptance test") + } + + tests := []uint16{8898} + + for _, guiPort := range tests { + t.Run(fmt.Sprintf("%d", guiPort), func(t *testing.T) { + cliPath, cliPathErr := getCliPath() + if cliPathErr != nil { + t.Errorf("failed to get cli path, err: %v", cliPathErr) + return + } + + tapCmdArgs := getDefaultTapCommandArgs() + + tapNamespace := getDefaultTapNamespace() + tapCmdArgs = append(tapCmdArgs, tapNamespace...) + + tapCmdArgs = append(tapCmdArgs, "-p", fmt.Sprintf("%d", guiPort)) + + tapCmd := exec.Command(cliPath, tapCmdArgs...) + t.Logf("running command: %v", tapCmd.String()) + + t.Cleanup(func() { + if err := cleanupCommand(tapCmd); err != nil { + t.Logf("failed to cleanup tap command, err: %v", err) + } + }) + + if err := tapCmd.Start(); err != nil { + t.Errorf("failed to start tap command, err: %v", err) + return + } + + apiServerUrl := getApiServerUrl(guiPort) + + if err := waitTapPodsReady(apiServerUrl); err != nil { + t.Errorf("failed to start tap pods on time, err: %v", err) + return + } + }) + } +} + +func TestTapAllNamespaces(t *testing.T) { + if testing.Short() { + t.Skip("ignored acceptance test") + } + + expectedPods := []struct{ + Name string + Namespace string + }{ + {Name: "httpbin", Namespace: "mizu-tests"}, + {Name: "httpbin", Namespace: "mizu-tests2"}, + } + + cliPath, cliPathErr := getCliPath() + if cliPathErr != nil { + t.Errorf("failed to get cli path, err: %v", cliPathErr) + return + } + + tapCmdArgs := getDefaultTapCommandArgs() + tapCmdArgs = append(tapCmdArgs, "-A") + + tapCmd := exec.Command(cliPath, tapCmdArgs...) + t.Logf("running command: %v", tapCmd.String()) + + t.Cleanup(func() { + if err := cleanupCommand(tapCmd); err != nil { + t.Logf("failed to cleanup tap command, err: %v", err) + } + }) + + if err := tapCmd.Start(); err != nil { + t.Errorf("failed to start tap command, err: %v", err) + return + } + + apiServerUrl := getApiServerUrl(defaultApiServerPort) + + if err := waitTapPodsReady(apiServerUrl); err != nil { + t.Errorf("failed to start tap pods on time, err: %v", err) + return + } + + podsUrl := fmt.Sprintf("%v/api/tapStatus", apiServerUrl) + requestResult, requestErr := executeHttpGetRequest(podsUrl) + if requestErr != nil { + t.Errorf("failed to get tap status, err: %v", requestErr) + return + } + + pods, err := getPods(requestResult) + if err != nil { + t.Errorf("failed to get pods, err: %v", err) + return + } + + for _, expectedPod := range expectedPods { + podFound := false + + for _, pod := range pods { + podNamespace := pod["namespace"].(string) + podName := pod["name"].(string) + + if expectedPod.Namespace == podNamespace && strings.Contains(podName, expectedPod.Name) { + podFound = true + break + } + } + + if !podFound { + t.Errorf("unexpected result - expected pod not found, pod namespace: %v, pod name: %v", expectedPod.Namespace, expectedPod.Name) + return + } + } +} + +func TestTapMultipleNamespaces(t *testing.T) { + if testing.Short() { + t.Skip("ignored acceptance test") + } + + expectedPods := []struct{ + Name string + Namespace string + }{ + {Name: "httpbin", Namespace: "mizu-tests"}, + {Name: "httpbin2", Namespace: "mizu-tests"}, + {Name: "httpbin", Namespace: "mizu-tests2"}, + } + + cliPath, cliPathErr := getCliPath() + if cliPathErr != nil { + t.Errorf("failed to get cli path, err: %v", cliPathErr) + return + } + + tapCmdArgs := getDefaultTapCommandArgs() + var namespacesCmd []string + for _, expectedPod := range expectedPods { + namespacesCmd = append(namespacesCmd, "-n", expectedPod.Namespace) + } + tapCmdArgs = append(tapCmdArgs, namespacesCmd...) + + tapCmd := exec.Command(cliPath, tapCmdArgs...) + t.Logf("running command: %v", tapCmd.String()) + + t.Cleanup(func() { + if err := cleanupCommand(tapCmd); err != nil { + t.Logf("failed to cleanup tap command, err: %v", err) + } + }) + + if err := tapCmd.Start(); err != nil { + t.Errorf("failed to start tap command, err: %v", err) + return + } + + apiServerUrl := getApiServerUrl(defaultApiServerPort) + + if err := waitTapPodsReady(apiServerUrl); err != nil { + t.Errorf("failed to start tap pods on time, err: %v", err) + return + } + + podsUrl := fmt.Sprintf("%v/api/tapStatus", apiServerUrl) + requestResult, requestErr := executeHttpGetRequest(podsUrl) + if requestErr != nil { + t.Errorf("failed to get tap status, err: %v", requestErr) + return + } + + pods, err := getPods(requestResult) + if err != nil { + t.Errorf("failed to get pods, err: %v", err) + return + } + + if len(expectedPods) != len(pods) { + t.Errorf("unexpected result - expected pods length: %v, actual pods length: %v", len(expectedPods), len(pods)) + return + } + + for _, expectedPod := range expectedPods { + podFound := false + + for _, pod := range pods { + podNamespace := pod["namespace"].(string) + podName := pod["name"].(string) + + if expectedPod.Namespace == podNamespace && strings.Contains(podName, expectedPod.Name) { + podFound = true + break + } + } + + if !podFound { + t.Errorf("unexpected result - expected pod not found, pod namespace: %v, pod name: %v", expectedPod.Namespace, expectedPod.Name) + return + } + } +} + +func TestTapRegex(t *testing.T) { + if testing.Short() { + t.Skip("ignored acceptance test") + } + + regexPodName := "httpbin2" + expectedPods := []struct{ + Name string + Namespace string + }{ + {Name: regexPodName, Namespace: "mizu-tests"}, + } + + cliPath, cliPathErr := getCliPath() + if cliPathErr != nil { + t.Errorf("failed to get cli path, err: %v", cliPathErr) + return + } + + tapCmdArgs := getDefaultTapCommandArgsWithRegex(regexPodName) + + tapNamespace := getDefaultTapNamespace() + tapCmdArgs = append(tapCmdArgs, tapNamespace...) + + tapCmd := exec.Command(cliPath, tapCmdArgs...) + t.Logf("running command: %v", tapCmd.String()) + + t.Cleanup(func() { + if err := cleanupCommand(tapCmd); err != nil { + t.Logf("failed to cleanup tap command, err: %v", err) + } + }) + + if err := tapCmd.Start(); err != nil { + t.Errorf("failed to start tap command, err: %v", err) + return + } + + apiServerUrl := getApiServerUrl(defaultApiServerPort) + + if err := waitTapPodsReady(apiServerUrl); err != nil { + t.Errorf("failed to start tap pods on time, err: %v", err) + return + } + + podsUrl := fmt.Sprintf("%v/api/tapStatus", apiServerUrl) + requestResult, requestErr := executeHttpGetRequest(podsUrl) + if requestErr != nil { + t.Errorf("failed to get tap status, err: %v", requestErr) + return + } + + pods, err := getPods(requestResult) + if err != nil { + t.Errorf("failed to get pods, err: %v", err) + return + } + + if len(expectedPods) != len(pods) { + t.Errorf("unexpected result - expected pods length: %v, actual pods length: %v", len(expectedPods), len(pods)) + return + } + + for _, expectedPod := range expectedPods { + podFound := false + + for _, pod := range pods { + podNamespace := pod["namespace"].(string) + podName := pod["name"].(string) + + if expectedPod.Namespace == podNamespace && strings.Contains(podName, expectedPod.Name) { + podFound = true + break + } + } + + if !podFound { + t.Errorf("unexpected result - expected pod not found, pod namespace: %v, pod name: %v", expectedPod.Namespace, expectedPod.Name) + return + } + } +} + +func TestTapDryRun(t *testing.T) { + if testing.Short() { + t.Skip("ignored acceptance test") + } + + cliPath, cliPathErr := getCliPath() + if cliPathErr != nil { + t.Errorf("failed to get cli path, err: %v", cliPathErr) + return + } + + tapCmdArgs := getDefaultTapCommandArgs() + + tapNamespace := getDefaultTapNamespace() + tapCmdArgs = append(tapCmdArgs, tapNamespace...) + + tapCmdArgs = append(tapCmdArgs, "--dry-run") + + tapCmd := exec.Command(cliPath, tapCmdArgs...) + t.Logf("running command: %v", tapCmd.String()) + + if err := tapCmd.Start(); err != nil { + t.Errorf("failed to start tap command, err: %v", err) + return + } + + resultChannel := make(chan string, 1) + + go func() { + if err := tapCmd.Wait(); err != nil { + resultChannel <- "fail" + return + } + resultChannel <- "success" + }() + + go func() { + time.Sleep(shortRetriesCount * time.Second) + resultChannel <- "fail" + }() + + testResult := <- resultChannel + if testResult != "success" { + t.Errorf("unexpected result - dry run cmd not done") + } +} + +func TestTapRedact(t *testing.T) { + if testing.Short() { + t.Skip("ignored acceptance test") + } + + cliPath, cliPathErr := getCliPath() + if cliPathErr != nil { + t.Errorf("failed to get cli path, err: %v", cliPathErr) + return + } + + tapCmdArgs := getDefaultTapCommandArgs() + + tapNamespace := getDefaultTapNamespace() + tapCmdArgs = append(tapCmdArgs, tapNamespace...) + + tapCmd := exec.Command(cliPath, tapCmdArgs...) + t.Logf("running command: %v", tapCmd.String()) + + t.Cleanup(func() { + if err := cleanupCommand(tapCmd); err != nil { + t.Logf("failed to cleanup tap command, err: %v", err) + } + }) + + if err := tapCmd.Start(); err != nil { + t.Errorf("failed to start tap command, err: %v", err) + return + } + + apiServerUrl := getApiServerUrl(defaultApiServerPort) + + if err := waitTapPodsReady(apiServerUrl); err != nil { + t.Errorf("failed to start tap pods on time, err: %v", err) + return + } + + proxyUrl := getProxyUrl(defaultNamespaceName, defaultServiceName) + requestBody := map[string]string{"User": "Mizu"} + for i := 0; i < defaultEntriesCount; i++ { + if _, requestErr := executeHttpPostRequest(fmt.Sprintf("%v/post", proxyUrl), requestBody); requestErr != nil { + t.Errorf("failed to send proxy request, err: %v", requestErr) + return + } + } + + redactCheckFunc := func() error { + timestamp := time.Now().UnixNano() / int64(time.Millisecond) + + entriesUrl := fmt.Sprintf("%v/api/entries?limit=%v&operator=lt×tamp=%v", apiServerUrl, defaultEntriesCount, timestamp) + requestResult, requestErr := executeHttpGetRequest(entriesUrl) + if requestErr != nil { + return fmt.Errorf("failed to get entries, err: %v", requestErr) + } + + entries := requestResult.([]interface{}) + firstEntry := entries[0].(map[string]interface{}) + + entryUrl := fmt.Sprintf("%v/api/entries/%v", apiServerUrl, firstEntry["id"]) + requestResult, requestErr = executeHttpGetRequest(entryUrl) + if requestErr != nil { + return fmt.Errorf("failed to get entry, err: %v", requestErr) + } + + entry := requestResult.(map[string]interface{})["entry"].(map[string]interface{}) + entryRequest := entry["request"].(map[string]interface{}) + + headers := entryRequest["headers"].([]interface{}) + for _, headerInterface := range headers { + header := headerInterface.(map[string]interface{}) + if header["name"].(string) != "User-Agent" { + continue + } + + userAgent := header["value"].(string) + if userAgent != "[REDACTED]" { + return fmt.Errorf("unexpected result - user agent is not redacted") + } + } + + data := entryRequest["postData"].(map[string]interface{}) + textDataStr := data["text"].(string) + + var textData map[string]string + if parseErr := json.Unmarshal([]byte(textDataStr), &textData); parseErr != nil { + return fmt.Errorf("failed to parse text data, err: %v", parseErr) + } + + if textData["User"] != "[REDACTED]" { + return fmt.Errorf("unexpected result - user in body is not redacted") + } + + return nil + } + if err := retriesExecute(shortRetriesCount, redactCheckFunc); err != nil { + t.Errorf("%v", err) + return + } +} + +func TestTapNoRedact(t *testing.T) { + if testing.Short() { + t.Skip("ignored acceptance test") + } + + cliPath, cliPathErr := getCliPath() + if cliPathErr != nil { + t.Errorf("failed to get cli path, err: %v", cliPathErr) + return + } + + tapCmdArgs := getDefaultTapCommandArgs() + + tapNamespace := getDefaultTapNamespace() + tapCmdArgs = append(tapCmdArgs, tapNamespace...) + + tapCmdArgs = append(tapCmdArgs, "--no-redact") + + tapCmd := exec.Command(cliPath, tapCmdArgs...) + t.Logf("running command: %v", tapCmd.String()) + + t.Cleanup(func() { + if err := cleanupCommand(tapCmd); err != nil { + t.Logf("failed to cleanup tap command, err: %v", err) + } + }) + + if err := tapCmd.Start(); err != nil { + t.Errorf("failed to start tap command, err: %v", err) + return + } + + apiServerUrl := getApiServerUrl(defaultApiServerPort) + + if err := waitTapPodsReady(apiServerUrl); err != nil { + t.Errorf("failed to start tap pods on time, err: %v", err) + return + } + + proxyUrl := getProxyUrl(defaultNamespaceName, defaultServiceName) + requestBody := map[string]string{"User": "Mizu"} + for i := 0; i < defaultEntriesCount; i++ { + if _, requestErr := executeHttpPostRequest(fmt.Sprintf("%v/post", proxyUrl), requestBody); requestErr != nil { + t.Errorf("failed to send proxy request, err: %v", requestErr) + return + } + } + + redactCheckFunc := func() error { + timestamp := time.Now().UnixNano() / int64(time.Millisecond) + + entriesUrl := fmt.Sprintf("%v/api/entries?limit=%v&operator=lt×tamp=%v", apiServerUrl, defaultEntriesCount, timestamp) + requestResult, requestErr := executeHttpGetRequest(entriesUrl) + if requestErr != nil { + return fmt.Errorf("failed to get entries, err: %v", requestErr) + } + + entries := requestResult.([]interface{}) + firstEntry := entries[0].(map[string]interface{}) + + entryUrl := fmt.Sprintf("%v/api/entries/%v", apiServerUrl, firstEntry["id"]) + requestResult, requestErr = executeHttpGetRequest(entryUrl) + if requestErr != nil { + return fmt.Errorf("failed to get entry, err: %v", requestErr) + } + + entry := requestResult.(map[string]interface{})["entry"].(map[string]interface{}) + entryRequest := entry["request"].(map[string]interface{}) + + headers := entryRequest["headers"].([]interface{}) + for _, headerInterface := range headers { + header := headerInterface.(map[string]interface{}) + if header["name"].(string) != "User-Agent" { + continue + } + + userAgent := header["value"].(string) + if userAgent == "[REDACTED]" { + return fmt.Errorf("unexpected result - user agent is redacted") + } + } + + data := entryRequest["postData"].(map[string]interface{}) + textDataStr := data["text"].(string) + + var textData map[string]string + if parseErr := json.Unmarshal([]byte(textDataStr), &textData); parseErr != nil { + return fmt.Errorf("failed to parse text data, err: %v", parseErr) + } + + if textData["User"] == "[REDACTED]" { + return fmt.Errorf("unexpected result - user in body is redacted") + } + + return nil + } + if err := retriesExecute(shortRetriesCount, redactCheckFunc); err != nil { + t.Errorf("%v", err) + return + } +} + +func TestTapRegexMasking(t *testing.T) { + if testing.Short() { + t.Skip("ignored acceptance test") + } + + cliPath, cliPathErr := getCliPath() + if cliPathErr != nil { + t.Errorf("failed to get cli path, err: %v", cliPathErr) + return + } + + tapCmdArgs := getDefaultTapCommandArgs() + + tapNamespace := getDefaultTapNamespace() + tapCmdArgs = append(tapCmdArgs, tapNamespace...) + + tapCmdArgs = append(tapCmdArgs, "-r", "Mizu") + + tapCmd := exec.Command(cliPath, tapCmdArgs...) + t.Logf("running command: %v", tapCmd.String()) + + t.Cleanup(func() { + if err := cleanupCommand(tapCmd); err != nil { + t.Logf("failed to cleanup tap command, err: %v", err) + } + }) + + if err := tapCmd.Start(); err != nil { + t.Errorf("failed to start tap command, err: %v", err) + return + } + + apiServerUrl := getApiServerUrl(defaultApiServerPort) + + if err := waitTapPodsReady(apiServerUrl); err != nil { + t.Errorf("failed to start tap pods on time, err: %v", err) + return + } + + proxyUrl := getProxyUrl(defaultNamespaceName, defaultServiceName) + for i := 0; i < defaultEntriesCount; i++ { + response, requestErr := http.Post(fmt.Sprintf("%v/post", proxyUrl), "text/plain", bytes.NewBufferString("Mizu")) + if _, requestErr = executeHttpRequest(response, requestErr); requestErr != nil { + t.Errorf("failed to send proxy request, err: %v", requestErr) + return + } + } + + redactCheckFunc := func() error { + timestamp := time.Now().UnixNano() / int64(time.Millisecond) + + entriesUrl := fmt.Sprintf("%v/api/entries?limit=%v&operator=lt×tamp=%v", apiServerUrl, defaultEntriesCount, timestamp) + requestResult, requestErr := executeHttpGetRequest(entriesUrl) + if requestErr != nil { + return fmt.Errorf("failed to get entries, err: %v", requestErr) + } + + entries := requestResult.([]interface{}) + firstEntry := entries[0].(map[string]interface{}) + + entryUrl := fmt.Sprintf("%v/api/entries/%v", apiServerUrl, firstEntry["id"]) + requestResult, requestErr = executeHttpGetRequest(entryUrl) + if requestErr != nil { + return fmt.Errorf("failed to get entry, err: %v", requestErr) + } + + entry := requestResult.(map[string]interface{})["entry"].(map[string]interface{}) + entryRequest := entry["request"].(map[string]interface{}) + + data := entryRequest["postData"].(map[string]interface{}) + textData := data["text"].(string) + + if textData != "[REDACTED]" { + return fmt.Errorf("unexpected result - body is not redacted") + } + + return nil + } + if err := retriesExecute(shortRetriesCount, redactCheckFunc); err != nil { + t.Errorf("%v", err) + return + } +} diff --git a/acceptanceTests/testsUtils.go b/acceptanceTests/testsUtils.go index edb2e8556..0d85a7353 100644 --- a/acceptanceTests/testsUtils.go +++ b/acceptanceTests/testsUtils.go @@ -1,8 +1,8 @@ package acceptanceTests import ( + "bytes" "encoding/json" - "errors" "fmt" "io/ioutil" "net/http" @@ -14,8 +14,12 @@ import ( ) const ( - LongRetriesCount = 100 - ShortRetriesCount = 10 + longRetriesCount = 100 + shortRetriesCount = 10 + defaultApiServerPort = 8899 + defaultNamespaceName = "mizu-tests" + defaultServiceName = "httpbin" + defaultEntriesCount = 50 ) func getCliPath() (string, error) { @@ -28,33 +32,64 @@ func getCliPath() (string, error) { return cliPath, nil } +func getConfigPath() (string, error) { + home, homeDirErr := os.UserHomeDir() + if homeDirErr != nil { + return "", homeDirErr + } + + return path.Join(home, ".mizu", "config.yaml"), nil +} + +func getProxyUrl(namespace string, service string) string { + return fmt.Sprintf("http://localhost:8080/api/v1/namespaces/%v/services/%v/proxy", namespace, service) +} + +func getApiServerUrl(port uint16) string { + return fmt.Sprintf("http://localhost:%v/mizu", port) +} + func getDefaultCommandArgs() []string { setFlag := "--set" telemetry := "telemetry=false" + agentImage := "agent-image=gcr.io/up9-docker-hub/mizu/ci:0.0.0" + imagePullPolicy := "image-pull-policy=Never" - return []string{setFlag, telemetry} + return []string{setFlag, telemetry, setFlag, agentImage, setFlag, imagePullPolicy} } func getDefaultTapCommandArgs() []string { tapCommand := "tap" - setFlag := "--set" - namespaces := "tap.namespaces=mizu-tests" - agentImage := "agent-image=gcr.io/up9-docker-hub/mizu/ci:0.0.0" - imagePullPolicy := "image-pull-policy=Never" - - defaultCmdArgs := getDefaultCommandArgs() - - return append([]string{tapCommand, setFlag, namespaces, setFlag, agentImage, setFlag, imagePullPolicy}, defaultCmdArgs...) -} - -func getDefaultFetchCommandArgs() []string { - tapCommand := "fetch" - defaultCmdArgs := getDefaultCommandArgs() return append([]string{tapCommand}, defaultCmdArgs...) } +func getDefaultTapCommandArgsWithRegex(regex string) []string { + tapCommand := "tap" + defaultCmdArgs := getDefaultCommandArgs() + + return append([]string{tapCommand, regex}, defaultCmdArgs...) +} + +func getDefaultTapNamespace() []string { + return []string{"-n", "mizu-tests"} +} + +func getDefaultFetchCommandArgs() []string { + fetchCommand := "fetch" + defaultCmdArgs := getDefaultCommandArgs() + + return append([]string{fetchCommand}, defaultCmdArgs...) +} + +func getDefaultConfigCommandArgs() []string { + configCommand := "config" + defaultCmdArgs := getDefaultCommandArgs() + + return append([]string{configCommand}, defaultCmdArgs...) +} + func retriesExecute(retriesCount int, executeFunc func() error) error { var lastError error @@ -72,19 +107,15 @@ func retriesExecute(retriesCount int, executeFunc func() error) error { return fmt.Errorf("reached max retries count, retries count: %v, last err: %v", retriesCount, lastError) } -func waitTapPodsReady() error { - resolvingUrl := fmt.Sprintf("http://localhost:8899/mizu/status/tappersCount") +func waitTapPodsReady(apiServerUrl string) error { + resolvingUrl := fmt.Sprintf("%v/status/tappersCount", apiServerUrl) tapPodsReadyFunc := func() error { - requestResult, requestErr := executeHttpRequest(resolvingUrl) + requestResult, requestErr := executeHttpGetRequest(resolvingUrl) if requestErr != nil { return requestErr } - tappersCount, ok := requestResult.(float64) - if !ok { - return fmt.Errorf("invalid tappers count type") - } - + tappersCount := requestResult.(float64) if tappersCount == 0 { return fmt.Errorf("no tappers running") } @@ -92,7 +123,7 @@ func waitTapPodsReady() error { return nil } - return retriesExecute(LongRetriesCount, tapPodsReadyFunc) + return retriesExecute(longRetriesCount, tapPodsReadyFunc) } func jsonBytesToInterface(jsonBytes []byte) (interface{}, error) { @@ -104,8 +135,7 @@ func jsonBytesToInterface(jsonBytes []byte) (interface{}, error) { return result, nil } -func executeHttpRequest(url string) (interface{}, error) { - response, requestErr := http.Get(url) +func executeHttpRequest(response *http.Response, requestErr error) (interface{}, error) { if requestErr != nil { return nil, requestErr } else if response.StatusCode != 200 { @@ -122,6 +152,21 @@ func executeHttpRequest(url string) (interface{}, error) { return jsonBytesToInterface(data) } +func executeHttpGetRequest(url string) (interface{}, error) { + response, requestErr := http.Get(url) + return executeHttpRequest(response, requestErr) +} + +func executeHttpPostRequest(url string, body interface{}) (interface{}, error) { + requestBody, jsonErr := json.Marshal(body) + if jsonErr != nil { + return nil, jsonErr + } + + response, requestErr := http.Post(url, "application/json", bytes.NewBuffer(requestBody)) + return executeHttpRequest(response, requestErr) +} + func cleanupCommand(cmd *exec.Cmd) error { if err := cmd.Process.Signal(syscall.SIGQUIT); err != nil { return err @@ -134,26 +179,27 @@ func cleanupCommand(cmd *exec.Cmd) error { return nil } -func getEntriesFromHarBytes(harBytes []byte) ([]interface{}, error){ +func getEntriesFromHarBytes(harBytes []byte) ([]interface{}, error) { harInterface, convertErr := jsonBytesToInterface(harBytes) if convertErr != nil { return nil, convertErr } - har, ok := harInterface.(map[string]interface{}) - if !ok { - return nil, errors.New("invalid har type") - } - - harLog, ok := har["log"].(map[string]interface{}) - if !ok { - return nil, errors.New("invalid har log type") - } - - harEntries, ok := harLog["entries"].([]interface{}) - if !ok { - return nil, errors.New("invalid har entries type") - } + har := harInterface.(map[string]interface{}) + harLog := har["log"].(map[string]interface{}) + harEntries := harLog["entries"].([]interface{}) return harEntries, nil } + +func getPods(tapStatusInterface interface{}) ([]map[string]interface{}, error) { + tapStatus := tapStatusInterface.(map[string]interface{}) + podsInterface := tapStatus["pods"].([]interface{}) + + var pods []map[string]interface{} + for _, podInterface := range podsInterface { + pods = append(pods, podInterface.(map[string]interface{})) + } + + return pods, nil +} diff --git a/cli/cmd/tapRunner.go b/cli/cmd/tapRunner.go index d19888e62..5c44b9348 100644 --- a/cli/cmd/tapRunner.go +++ b/cli/cmd/tapRunner.go @@ -561,7 +561,7 @@ func getNamespaces(kubernetesProvider *kubernetes.Provider) []string { if config.Config.Tap.AllNamespaces { return []string{mizu.K8sAllNamespaces} } else if len(config.Config.Tap.Namespaces) > 0 { - return config.Config.Tap.Namespaces + return mizu.Unique(config.Config.Tap.Namespaces) } else { return []string{kubernetesProvider.CurrentNamespace()} } diff --git a/cli/mizu/sliceUtils.go b/cli/mizu/sliceUtils.go index 551e12603..94e253225 100644 --- a/cli/mizu/sliceUtils.go +++ b/cli/mizu/sliceUtils.go @@ -9,3 +9,17 @@ func Contains(slice []string, containsValue string) bool { return false } + +func Unique(slice []string) []string { + keys := make(map[string]bool) + var list []string + + for _, entry := range slice { + if _, value := keys[entry]; !value { + keys[entry] = true + list = append(list, entry) + } + } + + return list +} diff --git a/cli/mizu/sliceUtils_test.go b/cli/mizu/sliceUtils_test.go index 49787c64c..d5e7efe6d 100644 --- a/cli/mizu/sliceUtils_test.go +++ b/cli/mizu/sliceUtils_test.go @@ -1,7 +1,9 @@ package mizu_test import ( + "fmt" "github.com/up9inc/mizu/cli/mizu" + "reflect" "testing" ) @@ -88,3 +90,41 @@ func TestContainsNilSlice(t *testing.T) { }) } } + +func TestUniqueNoDuplicateValues(t *testing.T) { + tests := []struct { + Slice []string + Expected []string + }{ + {Slice: []string{"apple", "orange", "banana", "grapes"}, Expected: []string{"apple", "orange", "banana", "grapes"}}, + {Slice: []string{"dog", "cat", "mouse"}, Expected: []string{"dog", "cat", "mouse"}}, + } + + for index, test := range tests { + t.Run(fmt.Sprintf("%v", index), func(t *testing.T) { + actual := mizu.Unique(test.Slice) + if !reflect.DeepEqual(test.Expected, actual) { + t.Errorf("unexpected result - Expected: %v, actual: %v", test.Expected, actual) + } + }) + } +} + +func TestUniqueDuplicateValues(t *testing.T) { + tests := []struct { + Slice []string + Expected []string + }{ + {Slice: []string{"apple", "apple", "orange", "orange", "banana", "banana", "grapes", "grapes"}, Expected: []string{"apple", "orange", "banana", "grapes"}}, + {Slice: []string{"dog", "cat", "cat", "mouse"}, Expected: []string{"dog", "cat", "mouse"}}, + } + + for index, test := range tests { + t.Run(fmt.Sprintf("%v", index), func(t *testing.T) { + actual := mizu.Unique(test.Slice) + if !reflect.DeepEqual(test.Expected, actual) { + t.Errorf("unexpected result - Expected: %v, actual: %v", test.Expected, actual) + } + }) + } +}