mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-30 15:05:27 +00:00
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.
This commit is contained in:
parent
ef479c1a6f
commit
9dd79321e0
@ -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",
|
||||
|
@ -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=<POD_IP>" \
|
||||
--env "NODE_IP=<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.
|
||||
|
@ -1 +1 @@
|
||||
2.7
|
||||
2.8
|
||||
|
@ -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)
|
||||
|
29
test/images/agnhost/guestbook/BUILD
Normal file
29
test/images/agnhost/guestbook/BUILD
Normal file
@ -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"],
|
||||
)
|
283
test/images/agnhost/guestbook/guestbook.go
Normal file
283
test/images/agnhost/guestbook/guestbook.go
Normal file
@ -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
|
||||
}
|
Loading…
Reference in New Issue
Block a user