diff --git a/cnf-certification-test/suite_test.go b/cnf-certification-test/suite_test.go index 22a900793..60b2325c4 100644 --- a/cnf-certification-test/suite_test.go +++ b/cnf-certification-test/suite_test.go @@ -17,10 +17,17 @@ package suite import ( + "bytes" _ "embed" + "encoding/json" "flag" + "fmt" + "io" + rlog "log" + "net/http" "os" "path/filepath" + "strings" "testing" "time" @@ -45,6 +52,7 @@ import ( _ "github.com/test-network-function/cnf-certification-test/cnf-certification-test/performance" _ "github.com/test-network-function/cnf-certification-test/cnf-certification-test/platform" _ "github.com/test-network-function/cnf-certification-test/cnf-certification-test/preflight" + "github.com/test-network-function/cnf-certification-test/cnf-certification-test/webserver" "github.com/test-network-function/cnf-certification-test/internal/clientsholder" "github.com/test-network-function/cnf-certification-test/pkg/configuration" "github.com/test-network-function/cnf-certification-test/pkg/diagnostics" @@ -56,8 +64,10 @@ const ( defaultClaimPath = ".." defaultCliArgValue = "" junitFlagKey = "junit" + serverModeFlag = "serverMode" TNFReportKey = "cnf-certification-test" extraInfoKey = "testsExtraInfo" + defaultServerMode = false ) var ( @@ -77,6 +87,7 @@ var ( // ClaimFormat is the current version for the claim file format to be produced by the TNF test suite. // A client decoding this claim file must support decoding its specific version. ClaimFormatVersion string + serverMode *bool ) func init() { @@ -84,6 +95,7 @@ func init() { "the path where the claimfile will be output") junitPath = flag.String(junitFlagKey, defaultCliArgValue, "the path for the junit format report") + serverMode = flag.Bool("serverMode", defaultServerMode, "run test with webserver") } // setLogLevel sets the log level for logrus based on the "TNF_LOG_LEVEL" environment variable @@ -126,8 +138,7 @@ func getGitVersion() string { return gitDisplayRelease + " ( " + GitCommit + " )" } -// TestTest invokes the CNF Certification Test Suite. -func TestTest(t *testing.T) { +func PreRun(t *testing.T) (claimData *claim.Claim, claimRoot *claim.Root) { // When running unit tests, skip the suite if os.Getenv("UNIT_TEST") != "" { t.Skip("Skipping test suite when running unit tests") @@ -147,20 +158,13 @@ func TestTest(t *testing.T) { log.Infof("Claim Format Version: %s", ClaimFormatVersion) log.Infof("Ginkgo Version : %v", ginkgo.GINKGO_VERSION) log.Infof("Labels filter : %v", ginkgoConfig.LabelFilter) - - // Diagnostic functions will run when no labels are provided. - var diagnosticMode bool - if ginkgoConfig.LabelFilter == "" { - log.Infof("TNF will run in diagnostic mode so no test case will be launched.") - diagnosticMode = true - } - + log.Infof("run test with webserver : %v", *serverMode) // Set clientsholder singleton with the filenames from the env vars. _ = clientsholder.GetClientsHolder(getK8sClientsConfigFileNames()...) // Initialize the claim with the start time, tnf version, etc. - claimRoot := claimhelper.CreateClaimRoot() - claimData := claimRoot.Claim + claimRoot = claimhelper.CreateClaimRoot() + claimData = claimRoot.Claim claimData.Configurations = make(map[string]interface{}) claimData.Nodes = make(map[string]interface{}) incorporateVersions(claimData) @@ -170,21 +174,42 @@ func TestTest(t *testing.T) { log.Errorf("Configuration node missing because of: %s", err) t.FailNow() } - claimData.Nodes = claimhelper.GenerateNodes() claimhelper.UnmarshalConfigurations(configurations, claimData.Configurations) + return claimData, claimRoot +} + +// TestTest invokes the CNF Certification Test Suite. +func TestTest(t *testing.T) { + ginkgoConfig, _ := ginkgo.GinkgoConfiguration() + if !*serverMode { + claimData, claimRoot := PreRun(t) + var diagnosticMode bool + // Diagnostic functions will run when no labels are provided. + + if ginkgoConfig.LabelFilter == "" { + log.Infof("TNF will run in diagnostic mode so no test case will be launched.") + diagnosticMode = true + } - // initialize abort flag - testhelper.AbortTrigger = "" + // initialize abort flag + testhelper.AbortTrigger = "" - // Run tests specs only if not in diagnostic mode, otherwise all TSs would run. - var env provider.TestEnvironment - if !diagnosticMode { - env.SetNeedsRefresh() - env = provider.GetTestEnvironment() - ginkgo.RunSpecs(t, CnfCertificationTestSuiteName) + // Run tests specs only if not in diagnostic mode, otherwise all TSs would run. + var env provider.TestEnvironment + if !diagnosticMode { + env.SetNeedsRefresh() + env = provider.GetTestEnvironment() + ginkgo.RunSpecs(t, CnfCertificationTestSuiteName) + } + ContinueRun(diagnosticMode, &env, claimData, claimRoot) + } else { + go StartServer() + select {} } +} +func ContinueRun(diagnosticMode bool, env *provider.TestEnvironment, claimData *claim.Claim, claimRoot *claim.Root) { endTime := time.Now() claimData.Metadata.EndTime = endTime.UTC().Format(claimhelper.DateTimeFormatDirective) @@ -211,7 +236,7 @@ func TestTest(t *testing.T) { // Send claim file to the collector if specified by env var if configuration.GetTestParameters().EnableDataCollection { - err = collector.SendClaimFileToCollector(env.CollectorAppEndPoint, claimOutputFile, env.ExecutedBy, env.PartnerName, env.CollectorAppPassword) + err := collector.SendClaimFileToCollector(env.CollectorAppEndPoint, claimOutputFile, env.ExecutedBy, env.PartnerName, env.CollectorAppPassword) if err != nil { log.Errorf("Failed to send post request to the collector: %v", err) } @@ -264,3 +289,102 @@ func incorporateVersions(claimData *claim.Claim) { ClaimFormat: ClaimFormatVersion, } } +func StartServer() { + server := &http.Server{ + Addr: ":8084", // Server address + ReadTimeout: 10 * time.Second, // Maximum duration for reading the entire request + WriteTimeout: 10 * time.Second, // Maximum duration for writing the entire response + IdleTimeout: 120 * time.Second, // Maximum idle duration before closing the connection + } + webserver.HandlereqFunc() + + http.HandleFunc("/runFunction", RunHandler) + + fmt.Println("Server is running on :8084...") + if err := server.ListenAndServe(); err != nil { + panic(err) + } +} + +// Define an HTTP handler that triggers Ginkgo tests +func RunHandler(w http.ResponseWriter, r *http.Request) { + webserver.Buf = bytes.NewBufferString("") + log.SetOutput(webserver.Buf) + rlog.SetOutput(webserver.Buf) + + jsonData := r.FormValue("jsonData") // "jsonData" is the name of the JSON input field + log.Info(jsonData) + var data webserver.RequestedData + if err := json.Unmarshal([]byte(jsonData), &data); err != nil { + fmt.Println("Error:", err) + } + var flattenedOptions []string + flattenedOptions = webserver.FlattenData(data.SelectedOptions, flattenedOptions) + + // Get the file from the request + file, handler, err := r.FormFile("kubeConfigPath") // "fileInput" is the name of the file input field + if err != nil { + http.Error(w, "Unable to retrieve file from form", http.StatusBadRequest) + return + } + defer file.Close() + + // Create a new file on the server to store the uploaded content + uploadedFile, err := os.Create(handler.Filename) + if err != nil { + http.Error(w, "Unable to create file for writing", http.StatusInternalServerError) + return + } + defer uploadedFile.Close() + + // Copy the uploaded file's content to the new file + _, err = io.Copy(uploadedFile, file) + if err != nil { + http.Error(w, "Unable to copy file", http.StatusInternalServerError) + return + } + + // Copy the uploaded file to the server file + + os.Setenv("KUBECONFIG", handler.Filename) + log.Infof("KUBECONFIG : %v", handler.Filename) + + log.Infof("Labels filter : %v", flattenedOptions) + t := testing.T{} + claimData, claimRoot := PreRun(&t) + var env provider.TestEnvironment + env.SetNeedsRefresh() + env = provider.GetTestEnvironment() + + // fetch the current config + suiteConfig, reporterConfig := ginkgo.GinkgoConfiguration() + // adjust it + suiteConfig.SkipStrings = []string{"NEVER-RUN"} + reporterConfig.FullTrace = true + reporterConfig.JUnitReport = "cnf-certification-tests_junit.xml" + // pass it in to RunSpecs + suiteConfig.LabelFilter = strings.Join(flattenedOptions, "") + ginkgo.RunSpecs(&t, CnfCertificationTestSuiteName, suiteConfig, reporterConfig) + + ContinueRun(false, &env, claimData, claimRoot) + // Return the result as JSON + response := struct { + Message string `json:"Message"` + }{ + Message: fmt.Sprintf("Sucsses to run %s", strings.Join(flattenedOptions, "")), + } + // Serialize the response data to JSON + jsonResponse, err := json.Marshal(response) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + // Set the Content-Type header to specify that the response is JSON + w.Header().Set("Content-Type", "application/json") + // Write the JSON response to the client + _, err = w.Write(jsonResponse) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } +} diff --git a/cnf-certification-test/webserver/index.html b/cnf-certification-test/webserver/index.html new file mode 100644 index 000000000..07611612a --- /dev/null +++ b/cnf-certification-test/webserver/index.html @@ -0,0 +1,238 @@ + + + + + + CNF Certification Test + + + + + + + + + + + + +
+ Red Hat +
+ +
+

CNF Certification Test

+ +
+ + +
+ Environment Configuration + + +
+ +
+ TNF Configuration + + + + + + +
+ +
+ Select a Test + + + +
+ + Run Certification Test +
+ Show Log +
+ + +

Logs

+ No Logs Found + Close +
+ + + + + + © 2022 Red Hat, Inc. + + + + + ({ ...acc, + [key]: key in acc ? [acc[key], val] : val + }), {}); + + delete fields.submit; + console.log(fields); + formdata.append("jsonData", JSON.stringify(fields)); + + // Send an HTTP request to the server to run the function + let heading; + let message; + let state = 'success'; + + try { + const data = await fetch('/runFunction', { + method: 'POST', + body: formdata, + }).then(response => { + if (response.ok) { + return response.json(); + } else { + throw new Error(response.statusText); + } + }); + + heading = 'Success'; + message = data.Message; + + console.log(data); + } catch (error) { + console.error(error); + heading = 'Error' + message = error.message; + state = 'danger'; + } finally { + form.elements.submit.disabled = false; + for (const el of form.elements) if (el instanceof HTMLFieldSetElement) el.disabled = false + } + + return { heading, message, state }; + } \ No newline at end of file diff --git a/cnf-certification-test/webserver/toast.js b/cnf-certification-test/webserver/toast.js new file mode 100644 index 000000000..1516826de --- /dev/null +++ b/cnf-certification-test/webserver/toast.js @@ -0,0 +1,53 @@ +import '@rhds/elements/rh-alert/rh-alert.js'; + +export async function toast({ + heading, + message, + state = 'info', + timeout = 8_000, +}) { + await import('@rhds/elements/rh-alert/rh-alert.js'); + const h2 = document.createElement('h2'); + h2.textContent = heading; + h2.slot = 'header'; + const alert = document.createElement('rh-alert'); + alert.setAttribute('aria-live', 'polite'); + alert.dismissable = true; + alert.state = state; + alert.classList.add('toast'); + alert.style.position = 'fixed'; + alert.style.margin = '0'; + alert.style.setProperty('z-index', '1000'); + alert.style.setProperty('inset-inline-end', 'var(--rh-space-xl, 24px)'); + alert.style.setProperty('inset-block-start', 'var(--rh-space-xl, 24px)'); + alert.append(h2); + if (message) { + const p = document.createElement('p'); + p.textContent = message; + alert.append(message); + } + + alert.animate({ translate: ['100% 0', '0 0'] }, { duration: 200 }); + + await Promise.all(Array.from(document.querySelectorAll('rh-alert.toast'), toast => + // TODO: handle more than 2 toasts + toast.animate({ + translate: [ + '0 auto', + '0 calc(100% + 20px)', + ], + }, { + duration: 200, + composite: 'accumulate', + rangeEnd: '100%', + fill: 'forwards', + }).finished)); + + setTimeout(() => { + if (alert.isConnected) { + alert.remove(); + } + }, timeout); + + document.body.append(alert); +} \ No newline at end of file diff --git a/cnf-certification-test/webserver/webserver_function.go b/cnf-certification-test/webserver/webserver_function.go new file mode 100644 index 000000000..1069432f6 --- /dev/null +++ b/cnf-certification-test/webserver/webserver_function.go @@ -0,0 +1,132 @@ +package webserver + +import ( + "bufio" + "bytes" + _ "embed" + "fmt" + "net/http" + + "github.com/gorilla/websocket" + "github.com/robert-nix/ansihtml" + "github.com/sirupsen/logrus" +) + +//go:embed index.html +var indexHTML []byte + +//go:embed submit.js +var submit []byte + +//go:embed logs.js +var logs []byte + +//go:embed toast.js +var toast []byte +var Buf *bytes.Buffer + +var upgrader = websocket.Upgrader{ + CheckOrigin: func(r *http.Request) bool { + return true + }, +} + +func logStreamHandler(w http.ResponseWriter, r *http.Request) { + conn, err := upgrader.Upgrade(w, r, nil) + if err != nil { + logrus.Printf("WebSocket upgrade error: %v", err) + return + } + defer conn.Close() + // Create a scanner to read the log file line by line + for { + scanner := bufio.NewScanner(Buf) + for scanner.Scan() { + line := scanner.Bytes() + line = append(ansihtml.ConvertToHTML(line), []byte("
")...) + + // Send each log line to the client + if err := conn.WriteMessage(websocket.TextMessage, line); err != nil { + fmt.Println(err) + return + } + } + if err := scanner.Err(); err != nil { + logrus.Printf("Error reading log file: %v", err) + return + } + } +} + +type RequestedData struct { + SelectedOptions interface{} `json:"selectedOptions"` +} +type ResponseData struct { + Message string `json:"message"` +} + +func FlattenData(data interface{}, result []string) []string { + switch v := data.(type) { + case string: + result = append(result, v) + case []interface{}: + for _, item := range v { + result = FlattenData(item, result) + } + case map[string]interface{}: + for key, item := range v { + if key == "selectedOptions" { + result = FlattenData(item, result) + } + result = FlattenData(item, result) + } + } + return result +} +func HandlereqFunc() { + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + // Set the content type to "text/html". + w.Header().Set("Content-Type", "text/html") + // Write the embedded HTML content to the response. + _, err := w.Write(indexHTML) + if err != nil { + http.Error(w, "Failed to write response", http.StatusInternalServerError) + return + } + }) + + http.HandleFunc("/submit.js", func(w http.ResponseWriter, r *http.Request) { + // Set the content type to "application/javascript". + w.Header().Set("Content-Type", "application/javascript") + // Write the embedded JavaScript content to the response. + _, err := w.Write(submit) + if err != nil { + http.Error(w, "Failed to write response", http.StatusInternalServerError) + return + } + }) + + http.HandleFunc("/logs.js", func(w http.ResponseWriter, r *http.Request) { + // Set the content type to "application/javascript". + w.Header().Set("Content-Type", "application/javascript") + // Write the embedded JavaScript content to the response. + _, err := w.Write(logs) + if err != nil { + http.Error(w, "Failed to write response", http.StatusInternalServerError) + return + } + }) + + http.HandleFunc("/toast.js", func(w http.ResponseWriter, r *http.Request) { + // Set the content type to "application/javascript". + w.Header().Set("Content-Type", "application/javascript") + // Write the embedded JavaScript content to the response. + _, err := w.Write(toast) + if err != nil { + http.Error(w, "Failed to write response", http.StatusInternalServerError) + return + } + }) + // Serve the static HTML file + http.HandleFunc("/logstream", logStreamHandler) +} diff --git a/go.mod b/go.mod index 32d9c88f8..a3d408080 100644 --- a/go.mod +++ b/go.mod @@ -211,12 +211,15 @@ require ( github.com/fatih/color v1.16.0 github.com/go-logr/logr v1.3.0 github.com/go-logr/stdr v1.2.2 + github.com/gorilla/websocket v1.4.2 github.com/k8snetworkplumbingwg/network-attachment-definition-client v1.4.0 github.com/manifoldco/promptui v0.9.0 github.com/openshift/machine-config-operator v0.0.1-0.20230515070935-49f32d46538e github.com/redhat-openshift-ecosystem/openshift-preflight v0.0.0-20231018165107-f04b78186455 + github.com/robert-nix/ansihtml v1.0.1 github.com/test-network-function/oct v0.0.3 github.com/test-network-function/privileged-daemonset v1.0.14 + github.com/robert-nix/ansihtml v1.0.1 gopkg.in/yaml.v3 v3.0.1 gotest.tools/v3 v3.5.1 k8s.io/kubectl v0.28.3 diff --git a/go.sum b/go.sum index f4b2c7deb..43965e69d 100644 --- a/go.sum +++ b/go.sum @@ -314,6 +314,7 @@ github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q= github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gosuri/uitable v0.0.4 h1:IG2xLKRvErL3uhY6e1BylFzG+aJiwQviDDTfOKeKTpY= github.com/gosuri/uitable v0.0.4/go.mod h1:tKR86bXuXPZazfOTG1FIzvjIdXzd0mo4Vtn16vt0PJo= @@ -517,6 +518,8 @@ github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qq github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/robert-nix/ansihtml v1.0.1 h1:VTiyQ6/+AxSJoSSLsMecnkh8i0ZqOEdiRl/odOc64fc= +github.com/robert-nix/ansihtml v1.0.1/go.mod h1:CJwclxYaTPc2RfcxtanEACsYuTksh4yDXcNeHHKZINE= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= diff --git a/run-cnf-suites.sh b/run-cnf-suites.sh index 7e7a0465d..625be8052 100755 --- a/run-cnf-suites.sh +++ b/run-cnf-suites.sh @@ -8,16 +8,17 @@ set -x export OUTPUT_LOC="$PWD/cnf-certification-test" usage() { - echo "$0 [-o OUTPUT_LOC] [-l LABEL...]" + echo "$0 [-o OUTPUT_LOC] [-l LABEL...] [-s run from webpage]" echo "Call the script and list the test suites to run" echo " e.g." echo " $0 [ARGS] -l \"access-control,lifecycle\"" echo " will run the access-control and lifecycle suites" echo " $0 [ARGS] -l all will run all the tests" + echo " $0 [ARGS] -s true will run the test from server" echo "" echo "Allowed suites are listed in the README." echo "" - echo "The specs can be listed with $0 -L|--list [-l LABEL...]" + echo "The specs can be listed with $0 -L|--list [-l LABEL...] [-s run from webpage]" } usage_error() { @@ -28,6 +29,7 @@ usage_error() { TIMEOUT=24h0m0s LABEL='' LIST=false +SERVER_RUN=false BASEDIR=$(dirname "$(realpath "$0")") # Parse args beginning with "-". @@ -49,6 +51,9 @@ while [[ $1 == -* ]]; do exit 1 fi ;; + -s) + SERVER_RUN=true + ;; -l | --label) while (("$#" >= 2)) && ! [[ $2 = --* ]] && ! [[ $2 = -* ]]; do LABEL="$LABEL $2" @@ -91,6 +96,10 @@ GINKGO_ARGS="\ -test.v\ " +if [ "$SERVER_RUN" = "true" ]; then + GINKGO_ARGS="$GINKGO_ARGS -serverMode" +fi + if [[ $LABEL == "all" ]]; then LABEL='common,extended,faredge,telco' fi @@ -100,7 +109,7 @@ echo "Report will be output to '$OUTPUT_LOC'" echo "ginkgo arguments '${GINKGO_ARGS}'" LABEL_STRING='' -if [ -z "$LABEL" ]; then +if [ -z "$LABEL" ] && { [ -z "$SERVER_RUN" ] || [ "$SERVER_RUN" == "false" ]; }; then echo "No test label (-l) was set, so only diagnostic functions will run." else LABEL_STRING="-ginkgo.label-filter=${LABEL}"