From 9dd79321e04705855de15c7bc43c90d0982e5888 Mon Sep 17 00:00:00 2001 From: Claudiu Belu Date: Fri, 20 Sep 2019 19:42:43 +0300 Subject: [PATCH] tests: Adds guestbook app subcommand in agnhost The redis version has been bumped to version 5.0.5, but the maximum version supported on Windows is 3.2. This can lead to failing tests, the output and behaviour can be different (see #80516). In order to prevent such failures, the amount of times the Redis image is used can be reduced. This commit adds the guestbook subcommand to agnhost, which can be used to emulate the Guestbook application created by the test "should create and stop a working application". Bumps agnhost image VERSION. --- test/images/agnhost/BUILD | 2 + test/images/agnhost/README.md | 46 +++- test/images/agnhost/VERSION | 2 +- test/images/agnhost/agnhost.go | 4 +- test/images/agnhost/guestbook/BUILD | 29 +++ test/images/agnhost/guestbook/guestbook.go | 283 +++++++++++++++++++++ 6 files changed, 357 insertions(+), 9 deletions(-) create mode 100644 test/images/agnhost/guestbook/BUILD create mode 100644 test/images/agnhost/guestbook/guestbook.go diff --git a/test/images/agnhost/BUILD b/test/images/agnhost/BUILD index f9faaaeeaaa..d8fd2c4dc72 100644 --- a/test/images/agnhost/BUILD +++ b/test/images/agnhost/BUILD @@ -22,6 +22,7 @@ go_library( "//test/images/agnhost/dns:go_default_library", "//test/images/agnhost/entrypoint-tester:go_default_library", "//test/images/agnhost/fakegitserver:go_default_library", + "//test/images/agnhost/guestbook:go_default_library", "//test/images/agnhost/inclusterclient:go_default_library", "//test/images/agnhost/liveness:go_default_library", "//test/images/agnhost/logs-generator:go_default_library", @@ -57,6 +58,7 @@ filegroup( "//test/images/agnhost/dns:all-srcs", "//test/images/agnhost/entrypoint-tester:all-srcs", "//test/images/agnhost/fakegitserver:all-srcs", + "//test/images/agnhost/guestbook:all-srcs", "//test/images/agnhost/inclusterclient:all-srcs", "//test/images/agnhost/liveness:all-srcs", "//test/images/agnhost/logs-generator:all-srcs", diff --git a/test/images/agnhost/README.md b/test/images/agnhost/README.md index 4e91d3cbf78..29687939fe1 100644 --- a/test/images/agnhost/README.md +++ b/test/images/agnhost/README.md @@ -40,7 +40,7 @@ For example, let's consider the following `pod.yaml` file: containers: - args: - dns-suffix - image: gcr.io/kubernetes-e2e-test-images/agnhost:2.7 + image: gcr.io/kubernetes-e2e-test-images/agnhost:2.8 name: agnhost dnsConfig: nameservers: @@ -189,6 +189,38 @@ Usage: ``` +### guestbook + +Starts a HTTP server on the given `--http-port` (default: 80), serving various endpoints representing a +guestbook app. The endpoints and their purpose are: + +- `/register`: A guestbook slave will subscribe to a master, to its given `--slaveof` endpoint. The master + will then push any updates it receives to its registered slaves through the `--backend-port` (default: 6379). +- `/get`: Returns `{"data": value}`, where the `value` is the stored value for the given `key` if non-empty, + or the entire store. +- `/set`: Will set the given key-value pair in its own store and propagate it to its slaves, if any. + Will return `{"data": "Updated"}` to the caller on success. +- `/guestbook`: Will proxy the request to `agnhost-master` if the given `cmd` is `set`, or `agnhost-slave` + if the given `cmd` is `get`. + +Usage: + +```console +guestbook="test/e2e/testing-manifests/guestbook" +sed_expr="s|{{.AgnhostImage}}|gcr.io/kubernetes-e2e-test-images/agnhost:2.8|" + +# create the services. +kubectl create -f ${guestbook}/frontend-service.yaml +kubectl create -f ${guestbook}/agnhost-master-service.yaml +kubectl create -f ${guestbook}/agnhost-slave-service.yaml + +# create the deployments. +cat ${guestbook}/frontend-deployment.yaml.in | sed ${sed_expr} | kubectl create -f - +cat ${guestbook}/agnhost-master-deployment.yaml.in | sed ${sed_expr} | kubectl create -f - +cat ${guestbook}/agnhost-slave-deployment.yaml.in | sed ${sed_expr} | kubectl create -f - +``` + + ### help Prints the binary's help menu. Additionally, it can be followed by another subcommand @@ -258,14 +290,14 @@ Examples: ```console docker run -i \ - gcr.io/kubernetes-e2e-test-images/agnhost:2.7 \ + gcr.io/kubernetes-e2e-test-images/agnhost:2.8 \ logs-generator --log-lines-total 10 --run-duration 1s ``` ```console kubectl run logs-generator \ --generator=run-pod/v1 \ - --image=gcr.io/kubernetes-e2e-test-images/agnhost:2.7 \ + --image=gcr.io/kubernetes-e2e-test-images/agnhost:2.8 \ --restart=Never \ -- logs-generator -t 10 -d 1s ``` @@ -392,7 +424,7 @@ Usage: ```console kubectl run test-agnhost \ --generator=run-pod/v1 \ - --image=gcr.io/kubernetes-e2e-test-images/agnhost:2.7 \ + --image=gcr.io/kubernetes-e2e-test-images/agnhost:2.8 \ --restart=Never \ --env "POD_IP=" \ --env "NODE_IP=" \ @@ -447,7 +479,7 @@ Usage: ```console kubectl run test-agnhost \ --generator=run-pod/v1 \ - --image=gcr.io/kubernetes-e2e-test-images/agnhost:2.7 \ + --image=gcr.io/kubernetes-e2e-test-images/agnhost:2.8 \ --restart=Never \ --env "BIND_ADDRESS=localhost" \ --env "BIND_PORT=8080" \ @@ -534,6 +566,6 @@ The image contains `iperf`. ## Image -The image can be found at `gcr.io/kubernetes-e2e-test-images/agnhost:2.7` for Linux -containers, and `e2eteam/agnhost:2.7` for Windows containers. In the future, the same +The image can be found at `gcr.io/kubernetes-e2e-test-images/agnhost:2.8` for Linux +containers, and `e2eteam/agnhost:2.8` for Windows containers. In the future, the same repository can be used for both OSes. diff --git a/test/images/agnhost/VERSION b/test/images/agnhost/VERSION index 1effb003408..a4412fa745d 100644 --- a/test/images/agnhost/VERSION +++ b/test/images/agnhost/VERSION @@ -1 +1 @@ -2.7 +2.8 diff --git a/test/images/agnhost/agnhost.go b/test/images/agnhost/agnhost.go index 01a45660eba..edd80a2e52e 100644 --- a/test/images/agnhost/agnhost.go +++ b/test/images/agnhost/agnhost.go @@ -28,6 +28,7 @@ import ( "k8s.io/kubernetes/test/images/agnhost/dns" "k8s.io/kubernetes/test/images/agnhost/entrypoint-tester" "k8s.io/kubernetes/test/images/agnhost/fakegitserver" + "k8s.io/kubernetes/test/images/agnhost/guestbook" "k8s.io/kubernetes/test/images/agnhost/inclusterclient" "k8s.io/kubernetes/test/images/agnhost/liveness" "k8s.io/kubernetes/test/images/agnhost/logs-generator" @@ -44,7 +45,7 @@ import ( ) func main() { - rootCmd := &cobra.Command{Use: "app", Version: "2.7"} + rootCmd := &cobra.Command{Use: "app", Version: "2.8"} rootCmd.AddCommand(auditproxy.CmdAuditProxy) rootCmd.AddCommand(connect.CmdConnect) @@ -54,6 +55,7 @@ func main() { rootCmd.AddCommand(dns.CmdEtcHosts) rootCmd.AddCommand(entrypoint.CmdEntrypointTester) rootCmd.AddCommand(fakegitserver.CmdFakeGitServer) + rootCmd.AddCommand(guestbook.CmdGuestbook) rootCmd.AddCommand(inclusterclient.CmdInClusterClient) rootCmd.AddCommand(liveness.CmdLiveness) rootCmd.AddCommand(logsgen.CmdLogsGenerator) diff --git a/test/images/agnhost/guestbook/BUILD b/test/images/agnhost/guestbook/BUILD new file mode 100644 index 00000000000..9cbe9f97c2a --- /dev/null +++ b/test/images/agnhost/guestbook/BUILD @@ -0,0 +1,29 @@ +package(default_visibility = ["//visibility:public"]) + +load( + "@io_bazel_rules_go//go:def.bzl", + "go_library", +) + +go_library( + name = "go_default_library", + srcs = ["guestbook.go"], + importpath = "k8s.io/kubernetes/test/images/agnhost/guestbook", + deps = [ + "//staging/src/k8s.io/apimachinery/pkg/util/net:go_default_library", + "//vendor/github.com/spf13/cobra:go_default_library", + ], +) + +filegroup( + name = "package-srcs", + srcs = glob(["**"]), + tags = ["automanaged"], + visibility = ["//visibility:private"], +) + +filegroup( + name = "all-srcs", + srcs = [":package-srcs"], + tags = ["automanaged"], +) diff --git a/test/images/agnhost/guestbook/guestbook.go b/test/images/agnhost/guestbook/guestbook.go new file mode 100644 index 00000000000..d4c0161f971 --- /dev/null +++ b/test/images/agnhost/guestbook/guestbook.go @@ -0,0 +1,283 @@ +/* +Copyright 2019 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package guestbook + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "log" + "net" + "net/http" + "net/url" + "strings" + "time" + + "github.com/spf13/cobra" + + utilnet "k8s.io/apimachinery/pkg/util/net" +) + +// CmdGuestbook is used by agnhost Cobra. +var CmdGuestbook = &cobra.Command{ + Use: "guestbook", + Short: "Creates a HTTP server with various endpoints representing a guestbook app", + Long: `Starts a HTTP server on the given --http-port (default: 80), serving various endpoints representing a guestbook app. The endpoints and their purpose are: + +- /register: A guestbook slave will subscribe to a master, to its given --slaveof endpoint. The master will then push any updates it receives to its registered slaves through the --backend-port. +- /get: Returns '{"data": value}', where the value is the stored value for the given key if non-empty, or the entire store. +- /set: Will set the given key-value pair in its own store and propagate it to its slaves, if any. Will return '{"data": "Updated"}' to the caller on success. +- /guestbook: Will proxy the request to agnhost-master if the given cmd is 'set', or agnhost-slave if the given cmd is 'get'.`, + Args: cobra.MaximumNArgs(0), + Run: main, +} + +var ( + httpPort string + backendPort string + slaveOf string + slaves []string + store map[string]interface{} +) + +const ( + timeout = time.Duration(15) * time.Second + sleep = time.Duration(1) * time.Second +) + +func init() { + CmdGuestbook.Flags().StringVar(&httpPort, "http-port", "80", "HTTP Listen Port") + CmdGuestbook.Flags().StringVar(&backendPort, "backend-port", "6379", "Backend's HTTP Listen Port") + CmdGuestbook.Flags().StringVar(&slaveOf, "slaveof", "", "The host's name to register to") + store = make(map[string]interface{}) +} + +func main(cmd *cobra.Command, args []string) { + go registerNode(slaveOf, backendPort) + startHTTPServer(httpPort) +} + +func registerNode(registerTo, port string) { + if registerTo == "" { + return + } + + hostPort := net.JoinHostPort(registerTo, backendPort) + _, err := net.ResolveTCPAddr("tcp", hostPort) + if err != nil { + log.Fatalf("--slaveof param and/or --backend-port param are invalid. %v", err) + return + } + + start := time.Now() + for time.Since(start) < timeout { + response, err := dialHTTP("register", hostPort) + if err != nil { + log.Printf("encountered error while registering to master: %v. Retrying in 1 second.", err) + time.Sleep(sleep) + continue + } + + responseJSON := make(map[string]interface{}) + err = json.Unmarshal([]byte(response), &responseJSON) + if err != nil { + log.Fatalf("Error while unmarshaling master's response: %v", err) + } + + responseJSON["data"] = "something" + var ok bool + store, ok = responseJSON["data"].(map[string]interface{}) + if !ok { + log.Fatalf("Could not cast responseJSON: %s", responseJSON["data"]) + } + log.Printf("Registered to node: %s", registerTo) + return + } + + log.Fatal("Timed out while registering to master.") +} + +func startHTTPServer(port string) { + http.HandleFunc("/register", registerHandler) + http.HandleFunc("/get", getHandler) + http.HandleFunc("/set", setHandler) + http.HandleFunc("/guestbook", guestbookHandler) + log.Fatal(http.ListenAndServe(fmt.Sprintf(":%s", port), nil)) +} + +// registerHandler will register the caller in this server's list of slaves. +// /set requests will be propagated to slaves, if any. +func registerHandler(w http.ResponseWriter, r *http.Request) { + ip, _, err := net.SplitHostPort(r.RemoteAddr) + if err != nil { + fmt.Fprintf(w, "userip: %q is not IP:port", r.RemoteAddr) + return + } + log.Printf("GET /register, IP: %s", ip) + + // send all the store to the slave as well. + output := make(map[string]interface{}) + output["data"] = store + bytes, err := json.Marshal(output) + if err != nil { + http.Error(w, fmt.Sprintf("response could not be serialized. %v", err), http.StatusExpectationFailed) + return + } + fmt.Fprint(w, string(bytes)) + slaves = append(slaves, ip) + log.Printf("Node '%s' registered.", ip) +} + +// getHandler will return '{"data": value}', where value is the stored value for +// the given key if non-empty, or entire store. +func getHandler(w http.ResponseWriter, r *http.Request) { + values, err := url.Parse(r.URL.RequestURI()) + if err != nil { + http.Error(w, fmt.Sprintf("%v", err), http.StatusBadRequest) + return + } + + key := values.Query().Get("key") + + log.Printf("GET /get?key=%s", key) + + output := make(map[string]interface{}) + if key == "" { + output["data"] = store + } else { + value, found := store[key] + if !found { + value = "" + } + output["data"] = value + } + + bytes, err := json.Marshal(output) + if err == nil { + fmt.Fprint(w, string(bytes)) + } else { + http.Error(w, fmt.Sprintf("response could not be serialized. %v", err), http.StatusExpectationFailed) + } +} + +// setHandler will set the given key-value pair in its own store and propagate +// it to its slaves, if any. Will return '{"message": "Updated"}' to the caller on success. +func setHandler(w http.ResponseWriter, r *http.Request) { + values, err := url.Parse(r.URL.RequestURI()) + if err != nil { + http.Error(w, fmt.Sprintf("%v", err), http.StatusBadRequest) + return + } + + key := values.Query().Get("key") + value := values.Query().Get("value") + + log.Printf("GET /set?key=%s&value=%s", key, value) + + if key == "" { + http.Error(w, "cannot set with empty key.", http.StatusBadRequest) + return + } + + store[key] = value + request := fmt.Sprintf("set?key=%s&value=%s", key, value) + for _, slave := range slaves { + hostPort := net.JoinHostPort(slave, backendPort) + _, err = dialHTTP(request, hostPort) + if err != nil { + http.Error(w, fmt.Sprintf("encountered error while propagating to slave '%s': %v", slave, err), http.StatusExpectationFailed) + return + } + } + + output := map[string]string{} + output["message"] = "Updated" + bytes, err := json.Marshal(output) + if err == nil { + fmt.Fprint(w, string(bytes)) + } else { + http.Error(w, fmt.Sprintf("response could not be serialized. %v", err), http.StatusExpectationFailed) + } +} + +// guestbookHandler will proxy the request to agnhost-master if the given cmd is +// 'set' or agnhost-slave if the given cmd is 'get'. +func guestbookHandler(w http.ResponseWriter, r *http.Request) { + values, err := url.Parse(r.URL.RequestURI()) + if err != nil { + http.Error(w, fmt.Sprintf("%v", err), http.StatusBadRequest) + return + } + + cmd := strings.ToLower(values.Query().Get("cmd")) + key := values.Query().Get("key") + value := values.Query().Get("value") + + log.Printf("GET /guestbook?cmd=%s&key=%s&value=%s", cmd, key, value) + + if cmd != "get" && cmd != "set" { + http.Error(w, fmt.Sprintf("unsupported cmd: '%s'", cmd), http.StatusBadRequest) + return + } + if cmd == "set" && key == "" { + http.Error(w, "cannot set with empty key.", http.StatusBadRequest) + return + } + + host := "agnhost-master" + if cmd == "get" { + host = "agnhost-slave" + } + + hostPort := net.JoinHostPort(host, backendPort) + _, err = net.ResolveTCPAddr("tcp", hostPort) + if err != nil { + http.Error(w, fmt.Sprintf("host and/or port param are invalid. %v", err), http.StatusBadRequest) + return + } + + request := fmt.Sprintf("%s?key=%s&value=%s", cmd, key, value) + response, err := dialHTTP(request, hostPort) + if err == nil { + fmt.Fprint(w, response) + } else { + http.Error(w, fmt.Sprintf("encountered error: %v", err), http.StatusExpectationFailed) + } +} + +func dialHTTP(request, hostPort string) (string, error) { + transport := utilnet.SetTransportDefaults(&http.Transport{}) + httpClient := createHTTPClient(transport) + resp, err := httpClient.Get(fmt.Sprintf("http://%s/%s", hostPort, request)) + defer transport.CloseIdleConnections() + if err == nil { + defer resp.Body.Close() + body, err := ioutil.ReadAll(resp.Body) + if err == nil { + return string(body), nil + } + } + return "", err +} + +func createHTTPClient(transport *http.Transport) *http.Client { + client := &http.Client{ + Transport: transport, + Timeout: 5 * time.Second, + } + return client +}