Merge pull request #83055 from bclau/tests/agnhost-guestbook-app

tests: Adds guestbook app subcommand in agnhost
This commit is contained in:
Kubernetes Prow Robot 2019-11-04 15:28:10 -08:00 committed by GitHub
commit 2999b8bebb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 357 additions and 9 deletions

View File

@ -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",

View File

@ -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.

View File

@ -1 +1 @@
2.7
2.8

View File

@ -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)

View 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"],
)

View 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
}