mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-22 19:31:44 +00:00
Merge pull request #83055 from bclau/tests/agnhost-guestbook-app
tests: Adds guestbook app subcommand in agnhost
This commit is contained in:
commit
2999b8bebb
@ -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