diff --git a/Makefile b/Makefile index 06e3990f6..5cfbf23bc 100644 --- a/Makefile +++ b/Makefile @@ -111,7 +111,7 @@ vet: .PHONY: sec sec: - go run $(COMMON_GOFLAGS) github.com/securego/gosec/v2/cmd/gosec -severity medium -confidence medium -exclude G304,G204,G107 -quiet ./tests/... + go run $(COMMON_GOFLAGS) github.com/securego/gosec/v2/cmd/gosec -severity medium -confidence medium -exclude G304,G204,G107 -quiet ./tests/integration/... ./tests/helper... ./tests/e2escenarios/... go run $(COMMON_GOFLAGS) github.com/securego/gosec/v2/cmd/gosec -severity medium -confidence medium -exclude G304,G204 -quiet ./cmd/... ./pkg/... .PHONY: clean diff --git a/tests/e2escenarios/e2e_test.go b/tests/e2escenarios/e2e_test.go index 24afae783..ac789032e 100644 --- a/tests/e2escenarios/e2e_test.go +++ b/tests/e2escenarios/e2e_test.go @@ -1,9 +1,8 @@ -//go:build linux || darwin || dragonfly || solaris || openbsd || netbsd || freebsd -// +build linux darwin dragonfly solaris openbsd netbsd freebsd - package e2escenarios import ( + "bytes" + "encoding/json" "fmt" "io" "net/http" @@ -29,7 +28,10 @@ var _ = Describe("E2E Test", func() { checkIfDevEnvIsUp := func(url, assertString string) { Eventually(func() string { resp, err := http.Get(fmt.Sprintf("http://%s", url)) - Expect(err).ToNot(HaveOccurred()) + if err != nil { + fmt.Fprintf(GinkgoWriter, "error while trying to GET %q: %v\n", url, err) + return "" + } defer resp.Body.Close() body, _ := io.ReadAll(resp.Body) @@ -57,10 +59,10 @@ var _ = Describe("E2E Test", func() { helper.SendLine(ctx, "JavaScript") helper.ExpectString(ctx, "Select project type") - helper.SendLine(ctx, "Node.js\n") + helper.SendLine(ctx, "Node.js") helper.ExpectString(ctx, "Which starter project do you want to use") - helper.SendLine(ctx, "nodejs-starter\n") + helper.SendLine(ctx, "nodejs-starter") helper.ExpectString(ctx, "Enter component name") helper.SendLine(ctx, componentName) @@ -173,11 +175,11 @@ var _ = Describe("E2E Test", func() { helper.ExpectString(ctx, "Project type: Node.js") helper.ExpectString(ctx, "Is this correct") - helper.SendLine(ctx, "\n") + helper.SendLine(ctx, "") helper.ExpectString(ctx, "Select container for which you want to change configuration?") - helper.SendLine(ctx, "\n") + helper.SendLine(ctx, "") helper.ExpectString(ctx, "Enter component name") @@ -270,4 +272,137 @@ var _ = Describe("E2E Test", func() { Eventually(string(commonVar.CliRunner.Run(getSVCArgs...).Out.Contents()), 60, 3).ShouldNot(ContainSubstring(serviceName)) }) }) + + Context("starting with non-empty Directory add Binding", func() { + componentName := helper.RandString(6) + + sendDataEntry := func(url string) map[string]interface{} { + values := map[string]interface{}{"name": "joe", + "location": "tokyo", + "age": 23, + } + json_data, err := json.Marshal(values) + Expect(err).To(BeNil()) + resp, err := http.Post(fmt.Sprintf("http://%s/api/newuser", url), "application/json", bytes.NewBuffer(json_data)) + Expect(err).To(BeNil()) + var res map[string]interface{} + err = json.NewDecoder(resp.Body).Decode(&res) + Expect(err).To(BeNil()) + return res + } + + receiveData := func(url string) (string, error) { + resp, err := http.Get(fmt.Sprintf("http://%s", url)) + if err != nil { + return "", err + } + defer resp.Body.Close() + body, err := io.ReadAll(resp.Body) + Expect(err).To(BeNil()) + return string(body), nil + } + + var _ = BeforeEach(func() { + commonVar.CliRunner.EnsureOperatorIsInstalled("service-binding-operator") + commonVar.CliRunner.EnsureOperatorIsInstalled("cloud-native-postgresql") + Eventually(func() string { + out, _ := commonVar.CliRunner.GetBindableKinds() + return out + }, 120, 3).Should(ContainSubstring("Cluster")) + helper.Chdir(commonVar.Context) + helper.CopyExample(filepath.Join("source", "devfiles", "go"), commonVar.Context) + addBindableKind := commonVar.CliRunner.Run("apply", "-f", helper.GetExamplePath("source", "devfiles", "go", "cluster.yaml")) + Expect(addBindableKind.ExitCode()).To(BeEquivalentTo(0)) + }) + + It("should verify developer workflow of using binding as env in innerloop", func() { + bindingName := helper.RandString(6) + + command := []string{"odo", "init"} + _, err := helper.RunInteractive(command, nil, func(ctx helper.InteractiveContext) { + + // helper.ExpectString(ctx, "Based on the files in the current directory odo detected") + helper.ExpectString(ctx, "Language: Go") + helper.ExpectString(ctx, "Project type: Go") + helper.ExpectString(ctx, "Is this correct") + + helper.SendLine(ctx, "") + + helper.ExpectString(ctx, "Select container for which you want to change configuration?") + + helper.SendLine(ctx, "") + + helper.ExpectString(ctx, "Enter component name") + + helper.SendLine(ctx, componentName) + + helper.ExpectString(ctx, "Your new component '"+componentName+"' is ready in the current directory") + + }) + Expect(err).To(BeNil()) + Expect(helper.ListFilesInDir(commonVar.Context)).To(ContainElement("devfile.yaml")) + + // // "execute odo dev and add changes to application" + var devSession helper.DevSession + var ports map[string]string + + devSession, _, _, ports, err = helper.StartDevMode(helper.DevSessionOpts{}) + Expect(err).ToNot(HaveOccurred()) + + // "send data" + _, err = receiveData(fmt.Sprintf(ports["8080"] + "/api/user")) + Expect(err).ToNot(BeNil()) // should fail as application is not connected to DB + + //add binding information (binding as ENV) + helper.Cmd("odo", "add", "binding", "--name", bindingName, "--service", "cluster-example-initdb", "--bind-as-files=false").ShouldPass() + + // Get new random port after restart + Eventually(func() map[string]string { + _, _, ports, err = devSession.GetInfo() + Expect(err).ToNot(HaveOccurred()) + return ports + }, 180, 10).ShouldNot(BeEmpty()) + + // "send data" + data := sendDataEntry(ports["8080"]) + Expect(data["message"]).To(Equal("User created successfully")) + + // "get all data" + rec, err := receiveData(fmt.Sprintf(ports["8080"] + "/api/user")) + Expect(err).To(BeNil()) + helper.MatchAllInOutput(rec, []string{"id", "1", "name", "joe", "location", "tokyo", "age", "23"}) + + // check odo describe to check for env + stdout := helper.Cmd("odo", "describe", "binding").ShouldPass().Out() + helper.MatchAllInOutput(stdout, []string{"Available binding information:", "CLUSTER_HOST", "CLUSTER_PASSWORD", "CLUSTER_USERNAME"}) + + // "running odo list" + stdout = helper.Cmd("odo", "list").ShouldPass().Out() + helper.MatchAllInOutput(stdout, []string{componentName, "Go", "Dev", bindingName}) + + // "exit dev mode" + devSession.Stop() + devSession.WaitEnd() + + // remove bindings and check devfile to not contain binding info + // TODO: move `remove binding` inside devsession after https://github.com/redhat-developer/odo/issues/6101 is fixed + helper.Cmd("odo", "remove", "binding", "--name", bindingName).ShouldPass() + + devSession, _, _, _, err = helper.StartDevMode(helper.DevSessionOpts{}) + Expect(err).To(BeNil()) + stdout = helper.Cmd("odo", "describe", "binding").ShouldPass().Out() + Expect(stdout).To(ContainSubstring("No ServiceBinding used by the current component")) + + devSession.Stop() + devSession.WaitEnd() + + // all resources should be deleted from the namespace + services := commonVar.CliRunner.GetServices(commonVar.Project) + Expect(services).NotTo(ContainSubstring(componentName)) + pvcs := commonVar.CliRunner.GetAllPVCNames(commonVar.Project) + Expect(pvcs).NotTo(ContainElement(componentName)) //To(Not(ContainSubstring(componentName))) + pods := commonVar.CliRunner.GetAllPodNames(commonVar.Project) + Expect(pods).NotTo(ContainElement(componentName)) + }) + }) }) diff --git a/tests/examples/source/devfiles/go/cluster.yaml b/tests/examples/source/devfiles/go/cluster.yaml new file mode 100644 index 000000000..6cae1e85e --- /dev/null +++ b/tests/examples/source/devfiles/go/cluster.yaml @@ -0,0 +1,34 @@ +apiVersion: postgresql.k8s.enterprisedb.io/v1 +kind: Cluster +metadata: + name: cluster-example-initdb +spec: + instances: 1 + bootstrap: + initdb: + database: appdb + owner: appuser + secret: + name: appuser-secret + postInitApplicationSQL: + - create table users (userid SERIAL PRIMARY KEY, name TEXT, age INT, location TEXT) + storage: + size: 1Gi +--- +apiVersion: v1 +stringData: + username: appuser + password: test-12password! +kind: Secret +metadata: + name: appuser-secret +type: kubernetes.io/basic-auth +--- +apiVersion: v1 +stringData: + username: appuser + password: test-12password! +kind: Secret +metadata: + name: cluster-example-initdb-appuser +type: kubernetes.io/basic-auth diff --git a/tests/examples/source/devfiles/go/go.mod b/tests/examples/source/devfiles/go/go.mod new file mode 100644 index 000000000..c141ee801 --- /dev/null +++ b/tests/examples/source/devfiles/go/go.mod @@ -0,0 +1,7 @@ +module go-postgres + +require ( + github.com/gorilla/mux v1.7.4 + github.com/joho/godotenv v1.3.0 + github.com/lib/pq v1.3.0 +) diff --git a/tests/examples/source/devfiles/go/go.sum b/tests/examples/source/devfiles/go/go.sum new file mode 100644 index 000000000..be3e60cec --- /dev/null +++ b/tests/examples/source/devfiles/go/go.sum @@ -0,0 +1,6 @@ +github.com/gorilla/mux v1.7.4 h1:VuZ8uybHlWmqV03+zRzdwKL4tUnIp1MAQtp1mIFE1bc= +github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= +github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= +github.com/lib/pq v1.3.0 h1:/qkRGz8zljWiDcFvgpwUpwIAPu3r07TDvs3Rws+o/pU= +github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= diff --git a/tests/examples/source/devfiles/go/main.go b/tests/examples/source/devfiles/go/main.go new file mode 100644 index 000000000..40517ad9f --- /dev/null +++ b/tests/examples/source/devfiles/go/main.go @@ -0,0 +1,15 @@ +package main + +import ( + "fmt" + "go-postgres/router" + "log" + "net/http" +) + +func main() { + r := router.Router() + fmt.Println("Starting server on the port 8080...") + + log.Fatal(http.ListenAndServe(":8080", r)) +} diff --git a/tests/examples/source/devfiles/go/middleware/handlers.go b/tests/examples/source/devfiles/go/middleware/handlers.go new file mode 100644 index 000000000..e6520e67b --- /dev/null +++ b/tests/examples/source/devfiles/go/middleware/handlers.go @@ -0,0 +1,348 @@ +package middleware + +import ( + "database/sql" + "encoding/json" + "fmt" + "go-postgres/models" + "log" + "net/http" + "os" + "strconv" + + "github.com/gorilla/mux" + + _ "github.com/lib/pq" +) + +// response format +type response struct { + ID int64 `json:"id,omitempty"` + Message string `json:"message,omitempty"` +} + +// create connection with postgres db +func createConnection() *sql.DB { + + // collect values form env + connStr := "postgres://" + os.Getenv("CLUSTER_USERNAME") + ":" + os.Getenv("CLUSTER_PASSWORD") + "@" + os.Getenv("CLUSTER_HOST") + "/" + os.Getenv("CLUSTER_DATABASE") + "?sslmode=disable" + + // Open the connection + db, err := sql.Open("postgres", connStr) + + if err != nil { + panic(err) + } + + // check the connection + err = db.Ping() + + if err != nil { + panic(err) + } + + fmt.Println("Successfully connected!") + // return the connection + return db +} + +// CreateUser create a user in the postgres db +func CreateUser(w http.ResponseWriter, r *http.Request) { + + // create an empty user of type models.User + var user models.User + + // decode the json request to user + err := json.NewDecoder(r.Body).Decode(&user) + + if err != nil { + log.Fatalf("Unable to decode the request body. %v", err) + } + + // call insert user function and pass the user + insertID := insertUser(user) + + // format a response object + res := response{ + ID: insertID, + Message: "User created successfully", + } + + // send the response + json.NewEncoder(w).Encode(res) +} + +// GetUser will return a single user by its id +func GetUser(w http.ResponseWriter, r *http.Request) { + // get the userid from the request params, key is "id" + params := mux.Vars(r) + + // convert the id type from string to int + id, err := strconv.Atoi(params["id"]) + + if err != nil { + log.Fatalf("Unable to convert the string into int. %v", err) + } + + // call the getUser function with user id to retrieve a single user + user, err := getUser(int64(id)) + + if err != nil { + log.Fatalf("Unable to get user. %v", err) + } + + // send the response + json.NewEncoder(w).Encode(user) +} + +// GetAllUser will return all the users +func GetAllUser(w http.ResponseWriter, r *http.Request) { + + // get all the users in the db + users, err := getAllUsers() + + if err != nil { + log.Fatalf("Unable to get all user. %v", err) + } + + // send all the users as response + json.NewEncoder(w).Encode(users) +} + +// UpdateUser update user's detail in the postgres db +func UpdateUser(w http.ResponseWriter, r *http.Request) { + + // get the userid from the request params, key is "id" + params := mux.Vars(r) + + // convert the id type from string to int + id, err := strconv.Atoi(params["id"]) + + if err != nil { + log.Fatalf("Unable to convert the string into int. %v", err) + } + + // create an empty user of type models.User + var user models.User + + // decode the json request to user + err = json.NewDecoder(r.Body).Decode(&user) + + if err != nil { + log.Fatalf("Unable to decode the request body. %v", err) + } + + // call update user to update the user + updatedRows := updateUser(int64(id), user) + + // format the message string + msg := fmt.Sprintf("User updated successfully. Total rows/record affected %v", updatedRows) + + // format the response message + res := response{ + ID: int64(id), + Message: msg, + } + + // send the response + json.NewEncoder(w).Encode(res) +} + +// DeleteUser delete user's detail in the postgres db +func DeleteUser(w http.ResponseWriter, r *http.Request) { + + // get the userid from the request params, key is "id" + params := mux.Vars(r) + + // convert the id in string to int + id, err := strconv.Atoi(params["id"]) + + if err != nil { + log.Fatalf("Unable to convert the string into int. %v", err) + } + + // call the deleteUser, convert the int to int64 + deletedRows := deleteUser(int64(id)) + + // format the message string + msg := fmt.Sprintf("User updated successfully. Total rows/record affected %v", deletedRows) + + // format the reponse message + res := response{ + ID: int64(id), + Message: msg, + } + + // send the response + json.NewEncoder(w).Encode(res) +} + +//------------------------- handler functions ---------------- +// insert one user in the DB +func insertUser(user models.User) int64 { + + // create the postgres db connection + db := createConnection() + + // close the db connection + defer db.Close() + + // create the insert sql query + // returning userid will return the id of the inserted user + sqlStatement := `INSERT INTO users (name, location, age) VALUES ($1, $2, $3) RETURNING userid` + + // the inserted id will store in this id + var id int64 + + // execute the sql statement + // Scan function will save the insert id in the id + err := db.QueryRow(sqlStatement, user.Name, user.Location, user.Age).Scan(&id) + + if err != nil { + log.Fatalf("Unable to execute the query. %v", err) + } + + fmt.Printf("Inserted a single record %v", id) + + // return the inserted id + return id +} + +// get one user from the DB by its userid +func getUser(id int64) (models.User, error) { + // create the postgres db connection + db := createConnection() + + // close the db connection + defer db.Close() + + // create a user of models.User type + var user models.User + + // create the select sql query + sqlStatement := `SELECT * FROM users WHERE userid=$1` + + // execute the sql statement + row := db.QueryRow(sqlStatement, id) + + // unmarshal the row object to user + err := row.Scan(&user.ID, &user.Name, &user.Age, &user.Location) + + switch err { + case sql.ErrNoRows: + fmt.Println("No rows were returned!") + return user, nil + case nil: + return user, nil + default: + log.Fatalf("Unable to scan the row. %v", err) + } + + // return empty user on error + return user, err +} + +// get one user from the DB by its userid +func getAllUsers() ([]models.User, error) { + // create the postgres db connection + db := createConnection() + + // close the db connection + defer db.Close() + + var users []models.User + + // create the select sql query + sqlStatement := `SELECT * FROM users` + + // execute the sql statement + rows, err := db.Query(sqlStatement) + + if err != nil { + log.Fatalf("Unable to execute the query. %v", err) + } + + // close the statement + defer rows.Close() + + // iterate over the rows + for rows.Next() { + var user models.User + + // unmarshal the row object to user + err = rows.Scan(&user.ID, &user.Name, &user.Age, &user.Location) + + if err != nil { + log.Fatalf("Unable to scan the row. %v", err) + } + + // append the user in the users slice + users = append(users, user) + + } + + // return empty user on error + return users, err +} + +// update user in the DB +func updateUser(id int64, user models.User) int64 { + + // create the postgres db connection + db := createConnection() + + // close the db connection + defer db.Close() + + // create the update sql query + sqlStatement := `UPDATE users SET name=$2, location=$3, age=$4 WHERE userid=$1` + + // execute the sql statement + res, err := db.Exec(sqlStatement, id, user.Name, user.Location, user.Age) + + if err != nil { + log.Fatalf("Unable to execute the query. %v", err) + } + + // check how many rows affected + rowsAffected, err := res.RowsAffected() + + if err != nil { + log.Fatalf("Error while checking the affected rows. %v", err) + } + + fmt.Printf("Total rows/record affected %v", rowsAffected) + + return rowsAffected +} + +// delete user in the DB +func deleteUser(id int64) int64 { + + // create the postgres db connection + db := createConnection() + + // close the db connection + defer db.Close() + + // create the delete sql query + sqlStatement := `DELETE FROM users WHERE userid=$1` + + // execute the sql statement + res, err := db.Exec(sqlStatement, id) + + if err != nil { + log.Fatalf("Unable to execute the query. %v", err) + } + + // check how many rows affected + rowsAffected, err := res.RowsAffected() + + if err != nil { + log.Fatalf("Error while checking the affected rows. %v", err) + } + + fmt.Printf("Total rows/record affected %v", rowsAffected) + + return rowsAffected +} diff --git a/tests/examples/source/devfiles/go/models/models.go b/tests/examples/source/devfiles/go/models/models.go new file mode 100644 index 000000000..d913e6e51 --- /dev/null +++ b/tests/examples/source/devfiles/go/models/models.go @@ -0,0 +1,9 @@ +package models + +// User schema of the user table +type User struct { + ID int64 `json:"id"` + Name string `json:"name"` + Location string `json:"location"` + Age int64 `json:"age"` +} diff --git a/tests/examples/source/devfiles/go/router/router.go b/tests/examples/source/devfiles/go/router/router.go new file mode 100644 index 000000000..807280ad6 --- /dev/null +++ b/tests/examples/source/devfiles/go/router/router.go @@ -0,0 +1,21 @@ +package router + +import ( + "go-postgres/middleware" + + "github.com/gorilla/mux" +) + +// Router is exported and used in main.go +func Router() *mux.Router { + + router := mux.NewRouter() + + router.HandleFunc("/api/user/{id}", middleware.GetUser).Methods("GET", "OPTIONS") + router.HandleFunc("/api/user", middleware.GetAllUser).Methods("GET", "OPTIONS") + router.HandleFunc("/api/newuser", middleware.CreateUser).Methods("POST", "OPTIONS") + router.HandleFunc("/api/user/{id}", middleware.UpdateUser).Methods("PUT", "OPTIONS") + router.HandleFunc("/api/deleteuser/{id}", middleware.DeleteUser).Methods("DELETE", "OPTIONS") + + return router +}