mirror of https://github.com/k3s-io/k3s
Add unit test framework for edit command
parent
67859efaec
commit
af3505f53c
|
@ -149,6 +149,7 @@ go_test(
|
||||||
"delete_test.go",
|
"delete_test.go",
|
||||||
"describe_test.go",
|
"describe_test.go",
|
||||||
"drain_test.go",
|
"drain_test.go",
|
||||||
|
"edit_test.go",
|
||||||
"exec_test.go",
|
"exec_test.go",
|
||||||
"expose_test.go",
|
"expose_test.go",
|
||||||
"get_test.go",
|
"get_test.go",
|
||||||
|
@ -165,6 +166,7 @@ go_test(
|
||||||
"top_test.go",
|
"top_test.go",
|
||||||
],
|
],
|
||||||
data = [
|
data = [
|
||||||
|
"testdata",
|
||||||
"//examples:config",
|
"//examples:config",
|
||||||
"//test/fixtures",
|
"//test/fixtures",
|
||||||
],
|
],
|
||||||
|
@ -190,6 +192,7 @@ go_test(
|
||||||
"//pkg/util/term:go_default_library",
|
"//pkg/util/term:go_default_library",
|
||||||
"//vendor:github.com/spf13/cobra",
|
"//vendor:github.com/spf13/cobra",
|
||||||
"//vendor:github.com/stretchr/testify/assert",
|
"//vendor:github.com/stretchr/testify/assert",
|
||||||
|
"//vendor:gopkg.in/yaml.v2",
|
||||||
"//vendor:k8s.io/apimachinery/pkg/api/equality",
|
"//vendor:k8s.io/apimachinery/pkg/api/equality",
|
||||||
"//vendor:k8s.io/apimachinery/pkg/api/errors",
|
"//vendor:k8s.io/apimachinery/pkg/api/errors",
|
||||||
"//vendor:k8s.io/apimachinery/pkg/api/meta",
|
"//vendor:k8s.io/apimachinery/pkg/api/meta",
|
||||||
|
@ -200,7 +203,9 @@ go_test(
|
||||||
"//vendor:k8s.io/apimachinery/pkg/runtime/serializer/json",
|
"//vendor:k8s.io/apimachinery/pkg/runtime/serializer/json",
|
||||||
"//vendor:k8s.io/apimachinery/pkg/runtime/serializer/streaming",
|
"//vendor:k8s.io/apimachinery/pkg/runtime/serializer/streaming",
|
||||||
"//vendor:k8s.io/apimachinery/pkg/types",
|
"//vendor:k8s.io/apimachinery/pkg/types",
|
||||||
|
"//vendor:k8s.io/apimachinery/pkg/util/diff",
|
||||||
"//vendor:k8s.io/apimachinery/pkg/util/intstr",
|
"//vendor:k8s.io/apimachinery/pkg/util/intstr",
|
||||||
|
"//vendor:k8s.io/apimachinery/pkg/util/sets",
|
||||||
"//vendor:k8s.io/apimachinery/pkg/util/strategicpatch",
|
"//vendor:k8s.io/apimachinery/pkg/util/strategicpatch",
|
||||||
"//vendor:k8s.io/apimachinery/pkg/util/wait",
|
"//vendor:k8s.io/apimachinery/pkg/util/wait",
|
||||||
"//vendor:k8s.io/apimachinery/pkg/watch",
|
"//vendor:k8s.io/apimachinery/pkg/watch",
|
||||||
|
@ -227,6 +232,7 @@ filegroup(
|
||||||
"//pkg/kubectl/cmd/rollout:all-srcs",
|
"//pkg/kubectl/cmd/rollout:all-srcs",
|
||||||
"//pkg/kubectl/cmd/set:all-srcs",
|
"//pkg/kubectl/cmd/set:all-srcs",
|
||||||
"//pkg/kubectl/cmd/templates:all-srcs",
|
"//pkg/kubectl/cmd/templates:all-srcs",
|
||||||
|
"//pkg/kubectl/cmd/testdata/edit:all-srcs",
|
||||||
"//pkg/kubectl/cmd/testing:all-srcs",
|
"//pkg/kubectl/cmd/testing:all-srcs",
|
||||||
"//pkg/kubectl/cmd/util:all-srcs",
|
"//pkg/kubectl/cmd/util:all-srcs",
|
||||||
],
|
],
|
||||||
|
|
|
@ -0,0 +1,281 @@
|
||||||
|
/*
|
||||||
|
Copyright 2017 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 cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
|
yaml "gopkg.in/yaml.v2"
|
||||||
|
|
||||||
|
"k8s.io/apimachinery/pkg/api/meta"
|
||||||
|
"k8s.io/apimachinery/pkg/util/diff"
|
||||||
|
"k8s.io/apimachinery/pkg/util/sets"
|
||||||
|
"k8s.io/client-go/rest/fake"
|
||||||
|
"k8s.io/kubernetes/pkg/api"
|
||||||
|
cmdtesting "k8s.io/kubernetes/pkg/kubectl/cmd/testing"
|
||||||
|
cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util"
|
||||||
|
"k8s.io/kubernetes/pkg/kubectl/resource"
|
||||||
|
)
|
||||||
|
|
||||||
|
type EditTestCase struct {
|
||||||
|
Description string `yaml:"description"`
|
||||||
|
// create or edit
|
||||||
|
Mode string `yaml:"mode"`
|
||||||
|
Args []string `yaml:"args"`
|
||||||
|
Filename string `yaml:"filename"`
|
||||||
|
Output string `yaml:"outputFormat"`
|
||||||
|
Namespace string `yaml:"namespace"`
|
||||||
|
ExpectedStdout []string `yaml:"expectedStdout"`
|
||||||
|
ExpectedStderr []string `yaml:"expectedStderr"`
|
||||||
|
ExpectedExitCode int `yaml:"expectedExitCode"`
|
||||||
|
|
||||||
|
Steps []EditStep `yaml:"steps"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type EditStep struct {
|
||||||
|
// edit or request
|
||||||
|
StepType string `yaml:"type"`
|
||||||
|
|
||||||
|
// only applies to request
|
||||||
|
RequestMethod string `yaml:"expectedMethod,omitempty"`
|
||||||
|
RequestPath string `yaml:"expectedPath,omitempty"`
|
||||||
|
RequestContentType string `yaml:"expectedContentType,omitempty"`
|
||||||
|
Input string `yaml:"expectedInput"`
|
||||||
|
|
||||||
|
// only applies to request
|
||||||
|
ResponseStatusCode int `yaml:"resultingStatusCode,omitempty"`
|
||||||
|
|
||||||
|
Output string `yaml:"resultingOutput"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEdit(t *testing.T) {
|
||||||
|
var (
|
||||||
|
name string
|
||||||
|
testcase EditTestCase
|
||||||
|
i int
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
const updateEnvVar = "UPDATE_EDIT_FIXTURE_DATA"
|
||||||
|
updateInputFixtures := os.Getenv(updateEnvVar) == "true"
|
||||||
|
|
||||||
|
reqResp := func(req *http.Request) (*http.Response, error) {
|
||||||
|
defer func() { i++ }()
|
||||||
|
if i > len(testcase.Steps)-1 {
|
||||||
|
t.Fatalf("%s, step %d: more requests than steps, got %s %s", name, i, req.Method, req.URL.Path)
|
||||||
|
}
|
||||||
|
step := testcase.Steps[i]
|
||||||
|
|
||||||
|
body := []byte{}
|
||||||
|
if req.Body != nil {
|
||||||
|
body, err = ioutil.ReadAll(req.Body)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%s, step %d: %v", name, i, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inputFile := filepath.Join("testdata/edit", "testcase-"+name, step.Input)
|
||||||
|
expectedInput, err := ioutil.ReadFile(inputFile)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%s, step %d: %v", name, i, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
outputFile := filepath.Join("testdata/edit", "testcase-"+name, step.Output)
|
||||||
|
resultingOutput, err := ioutil.ReadFile(outputFile)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%s, step %d: %v", name, i, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Method == "POST" && req.URL.Path == "/callback" {
|
||||||
|
if step.StepType != "edit" {
|
||||||
|
t.Fatalf("%s, step %d: expected edit step, got %s %s", name, i, req.Method, req.URL.Path)
|
||||||
|
}
|
||||||
|
if bytes.Compare(body, expectedInput) != 0 {
|
||||||
|
if updateInputFixtures {
|
||||||
|
// Convenience to allow recapturing the input and persisting it here
|
||||||
|
ioutil.WriteFile(inputFile, body, os.FileMode(0644))
|
||||||
|
} else {
|
||||||
|
t.Errorf("%s, step %d: diff in edit content:\n%s", name, i, diff.StringDiff(string(body), string(expectedInput)))
|
||||||
|
t.Logf("If the change in input is expected, rerun tests with %s=true to update input fixtures", updateEnvVar)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &http.Response{StatusCode: 200, Body: ioutil.NopCloser(bytes.NewReader(resultingOutput))}, nil
|
||||||
|
} else {
|
||||||
|
if step.StepType != "request" {
|
||||||
|
t.Fatalf("%s, step %d: expected request step, got %s %s", name, i, req.Method, req.URL.Path)
|
||||||
|
}
|
||||||
|
body = tryIndent(body)
|
||||||
|
expectedInput = tryIndent(expectedInput)
|
||||||
|
if req.Method != step.RequestMethod || req.URL.Path != step.RequestPath || req.Header.Get("Content-Type") != step.RequestContentType {
|
||||||
|
t.Fatalf(
|
||||||
|
"%s, step %d: expected \n%s %s (content-type=%s)\ngot\n%s %s (content-type=%s)", name, i,
|
||||||
|
step.RequestMethod, step.RequestPath, step.RequestContentType,
|
||||||
|
req.Method, req.URL.Path, req.Header.Get("Content-Type"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if bytes.Compare(body, expectedInput) != 0 {
|
||||||
|
if updateInputFixtures {
|
||||||
|
// Convenience to allow recapturing the input and persisting it here
|
||||||
|
ioutil.WriteFile(inputFile, body, os.FileMode(0644))
|
||||||
|
} else {
|
||||||
|
t.Errorf("%s, step %d: diff in edit content:\n%s", name, i, diff.StringDiff(string(body), string(expectedInput)))
|
||||||
|
t.Logf("If the change in input is expected, rerun tests with %s=true to update input fixtures", updateEnvVar)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &http.Response{StatusCode: step.ResponseStatusCode, Header: defaultHeader(), Body: ioutil.NopCloser(bytes.NewReader(resultingOutput))}, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handler := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||||
|
resp, _ := reqResp(req)
|
||||||
|
for k, vs := range resp.Header {
|
||||||
|
w.Header().Del(k)
|
||||||
|
for _, v := range vs {
|
||||||
|
w.Header().Add(k, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
w.WriteHeader(resp.StatusCode)
|
||||||
|
io.Copy(w, resp.Body)
|
||||||
|
})
|
||||||
|
|
||||||
|
server := httptest.NewServer(handler)
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
os.Setenv("KUBE_EDITOR", "testdata/edit/test_editor.sh")
|
||||||
|
os.Setenv("KUBE_EDITOR_CALLBACK", server.URL+"/callback")
|
||||||
|
|
||||||
|
testcases := sets.NewString()
|
||||||
|
filepath.Walk("testdata/edit", func(path string, info os.FileInfo, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if path == "testdata/edit" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
name := filepath.Base(path)
|
||||||
|
if info.IsDir() {
|
||||||
|
if strings.HasPrefix(name, "testcase-") {
|
||||||
|
testcases.Insert(strings.TrimPrefix(name, "testcase-"))
|
||||||
|
}
|
||||||
|
return filepath.SkipDir
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
// sanity check that we found the right folder
|
||||||
|
if !testcases.Has("create-list") {
|
||||||
|
t.Fatalf("Error locating edit testcases")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, testcaseName := range testcases.List() {
|
||||||
|
t.Logf("Running testcase: %s", testcaseName)
|
||||||
|
i = 0
|
||||||
|
name = testcaseName
|
||||||
|
testcase = EditTestCase{}
|
||||||
|
testcaseDir := filepath.Join("testdata", "edit", "testcase-"+name)
|
||||||
|
testcaseData, err := ioutil.ReadFile(filepath.Join(testcaseDir, "test.yaml"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%s: %v", name, err)
|
||||||
|
}
|
||||||
|
if err := yaml.Unmarshal(testcaseData, &testcase); err != nil {
|
||||||
|
t.Fatalf("%s: %v", name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
f, tf, _, ns := cmdtesting.NewAPIFactory()
|
||||||
|
tf.Printer = &testPrinter{}
|
||||||
|
tf.ClientForMappingFunc = func(mapping *meta.RESTMapping) (resource.RESTClient, error) {
|
||||||
|
versionedAPIPath := ""
|
||||||
|
if mapping.GroupVersionKind.Group == "" {
|
||||||
|
versionedAPIPath = "/api/" + mapping.GroupVersionKind.Version
|
||||||
|
} else {
|
||||||
|
versionedAPIPath = "/apis/" + mapping.GroupVersionKind.Group + "/" + mapping.GroupVersionKind.Version
|
||||||
|
}
|
||||||
|
return &fake.RESTClient{
|
||||||
|
APIRegistry: api.Registry,
|
||||||
|
VersionedAPIPath: versionedAPIPath,
|
||||||
|
NegotiatedSerializer: ns, //unstructuredSerializer,
|
||||||
|
Client: fake.CreateHTTPClient(reqResp),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(testcase.Namespace) > 0 {
|
||||||
|
tf.Namespace = testcase.Namespace
|
||||||
|
}
|
||||||
|
tf.ClientConfig = defaultClientConfig()
|
||||||
|
buf := bytes.NewBuffer([]byte{})
|
||||||
|
errBuf := bytes.NewBuffer([]byte{})
|
||||||
|
|
||||||
|
var cmd *cobra.Command
|
||||||
|
switch testcase.Mode {
|
||||||
|
case "edit":
|
||||||
|
cmd = NewCmdEdit(f, buf, errBuf)
|
||||||
|
case "create":
|
||||||
|
cmd = NewCmdCreate(f, buf, errBuf)
|
||||||
|
cmd.Flags().Set("edit", "true")
|
||||||
|
default:
|
||||||
|
t.Errorf("%s: unexpected mode %s", name, testcase.Mode)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if len(testcase.Filename) > 0 {
|
||||||
|
cmd.Flags().Set("filename", filepath.Join(testcaseDir, testcase.Filename))
|
||||||
|
}
|
||||||
|
if len(testcase.Output) > 0 {
|
||||||
|
cmd.Flags().Set("output", testcase.Output)
|
||||||
|
}
|
||||||
|
|
||||||
|
cmdutil.BehaviorOnFatal(func(str string, code int) {
|
||||||
|
errBuf.WriteString(str)
|
||||||
|
if testcase.ExpectedExitCode != code {
|
||||||
|
t.Errorf("%s: expected exit code %d, got %d: %s", name, testcase.ExpectedExitCode, code, str)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
cmd.Run(cmd, testcase.Args)
|
||||||
|
|
||||||
|
stdout := buf.String()
|
||||||
|
stderr := errBuf.String()
|
||||||
|
|
||||||
|
for _, s := range testcase.ExpectedStdout {
|
||||||
|
if !strings.Contains(stdout, s) {
|
||||||
|
t.Errorf("%s: expected to see '%s' in stdout\n\nstdout:\n%s\n\nstderr:\n%s", name, s, stdout, stderr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, s := range testcase.ExpectedStderr {
|
||||||
|
if !strings.Contains(stderr, s) {
|
||||||
|
t.Errorf("%s: expected to see '%s' in stderr\n\nstdout:\n%s\n\nstderr:\n%s", name, s, stdout, stderr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func tryIndent(data []byte) []byte {
|
||||||
|
indented := &bytes.Buffer{}
|
||||||
|
if err := json.Indent(indented, data, "", "\t"); err == nil {
|
||||||
|
return indented.Bytes()
|
||||||
|
}
|
||||||
|
return data
|
||||||
|
}
|
|
@ -0,0 +1,35 @@
|
||||||
|
package(default_visibility = ["//visibility:public"])
|
||||||
|
|
||||||
|
licenses(["notice"])
|
||||||
|
|
||||||
|
load(
|
||||||
|
"@io_bazel_rules_go//go:def.bzl",
|
||||||
|
"go_binary",
|
||||||
|
"go_library",
|
||||||
|
)
|
||||||
|
|
||||||
|
go_binary(
|
||||||
|
name = "edit",
|
||||||
|
library = ":go_default_library",
|
||||||
|
tags = ["automanaged"],
|
||||||
|
)
|
||||||
|
|
||||||
|
go_library(
|
||||||
|
name = "go_default_library",
|
||||||
|
srcs = ["record.go"],
|
||||||
|
tags = ["automanaged"],
|
||||||
|
deps = ["//vendor:gopkg.in/yaml.v2"],
|
||||||
|
)
|
||||||
|
|
||||||
|
filegroup(
|
||||||
|
name = "package-srcs",
|
||||||
|
srcs = glob(["**"]),
|
||||||
|
tags = ["automanaged"],
|
||||||
|
visibility = ["//visibility:private"],
|
||||||
|
)
|
||||||
|
|
||||||
|
filegroup(
|
||||||
|
name = "all-srcs",
|
||||||
|
srcs = [":package-srcs"],
|
||||||
|
tags = ["automanaged"],
|
||||||
|
)
|
|
@ -0,0 +1,22 @@
|
||||||
|
This folder contains test cases for interactive edit, and helpers for recording new test cases
|
||||||
|
|
||||||
|
To record a new test:
|
||||||
|
|
||||||
|
1. Start a local cluster running unsecured on http://localhost:8080 (e.g. hack/local-up-cluster.sh)
|
||||||
|
2. Set up any pre-existing resources you want to be available on that server (namespaces, resources to edit, etc)
|
||||||
|
3. Run ./pkg/kubectl/cmd/testdata/edit/record_testcase.sh my-testcase
|
||||||
|
4. Run the desired `kubectl edit ...` command, and interact with the editor as desired until it completes.
|
||||||
|
* You can do things that cause errors to appear in the editor (change immutable fields, fail validation, etc)
|
||||||
|
* You can perform edit flows that invoke the editor multiple times
|
||||||
|
* You can make out-of-band changes to the server resources that cause conflict errors to be returned
|
||||||
|
* The API requests/responses and editor inputs/outputs are captured in your testcase folder
|
||||||
|
5. Type exit.
|
||||||
|
6. Inspect the captured requests/responses and inputs/outputs for sanity
|
||||||
|
7. Modify the generated test.yaml file:
|
||||||
|
* Set a description of what the test is doing
|
||||||
|
* Enter the args (if any) you invoked edit with
|
||||||
|
* Enter the filename (if any) you invoked edit with
|
||||||
|
* Enter the output format (if any) you invoked edit with
|
||||||
|
* Optionally specify substrings to look for in the stdout or stderr of the edit command
|
||||||
|
8. Add your new testcase name to the list of testcases in edit_test.go
|
||||||
|
9. Run `go test ./pkg/kubectl/cmd -run TestEdit -v` to run edit tests
|
|
@ -0,0 +1,169 @@
|
||||||
|
/*
|
||||||
|
Copyright 2017 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 main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
yaml "gopkg.in/yaml.v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type EditTestCase struct {
|
||||||
|
Description string `yaml:"description"`
|
||||||
|
// create or edit
|
||||||
|
Mode string `yaml:"mode"`
|
||||||
|
Args []string `yaml:"args"`
|
||||||
|
Filename string `yaml:"filename"`
|
||||||
|
Output string `yaml:"outputFormat"`
|
||||||
|
Namespace string `yaml:"namespace"`
|
||||||
|
ExpectedStdout []string `yaml:"expectedStdout"`
|
||||||
|
ExpectedStderr []string `yaml:"expectedStderr"`
|
||||||
|
ExpectedExitCode int `yaml:"expectedExitCode"`
|
||||||
|
|
||||||
|
Steps []EditStep `yaml:"steps"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type EditStep struct {
|
||||||
|
// edit or request
|
||||||
|
StepType string `yaml:"type"`
|
||||||
|
|
||||||
|
// only applies to request
|
||||||
|
RequestMethod string `yaml:"expectedMethod,omitempty"`
|
||||||
|
RequestPath string `yaml:"expectedPath,omitempty"`
|
||||||
|
RequestContentType string `yaml:"expectedContentType,omitempty"`
|
||||||
|
Input string `yaml:"expectedInput"`
|
||||||
|
|
||||||
|
// only applies to request
|
||||||
|
ResponseStatusCode int `yaml:"resultingStatusCode,omitempty"`
|
||||||
|
|
||||||
|
Output string `yaml:"resultingOutput"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
tc := &EditTestCase{
|
||||||
|
Description: "add a testcase description",
|
||||||
|
Mode: "edit",
|
||||||
|
Args: []string{"set", "args"},
|
||||||
|
ExpectedStdout: []string{"expected stdout substring"},
|
||||||
|
ExpectedStderr: []string{"expected stderr substring"},
|
||||||
|
}
|
||||||
|
|
||||||
|
var currentStep *EditStep
|
||||||
|
|
||||||
|
fmt.Println(http.ListenAndServe(":8081", http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||||
|
|
||||||
|
// Record non-discovery things
|
||||||
|
record := false
|
||||||
|
switch segments := strings.Split(strings.Trim(req.URL.Path, "/"), "/"); segments[0] {
|
||||||
|
case "api":
|
||||||
|
// api, version
|
||||||
|
record = len(segments) > 2
|
||||||
|
case "apis":
|
||||||
|
// apis, group, version
|
||||||
|
record = len(segments) > 3
|
||||||
|
case "callback":
|
||||||
|
record = true
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := ioutil.ReadAll(req.Body)
|
||||||
|
checkErr(err)
|
||||||
|
|
||||||
|
switch m, p := req.Method, req.URL.Path; {
|
||||||
|
case m == "POST" && p == "/callback/in":
|
||||||
|
if currentStep != nil {
|
||||||
|
panic("cannot post input with step already in progress")
|
||||||
|
}
|
||||||
|
filename := fmt.Sprintf("%d.original", len(tc.Steps))
|
||||||
|
checkErr(ioutil.WriteFile(filename, body, os.FileMode(0755)))
|
||||||
|
currentStep = &EditStep{StepType: "edit", Input: filename}
|
||||||
|
case m == "POST" && p == "/callback/out":
|
||||||
|
if currentStep == nil || currentStep.StepType != "edit" {
|
||||||
|
panic("cannot post output without posting input first")
|
||||||
|
}
|
||||||
|
filename := fmt.Sprintf("%d.edited", len(tc.Steps))
|
||||||
|
checkErr(ioutil.WriteFile(filename, body, os.FileMode(0755)))
|
||||||
|
currentStep.Output = filename
|
||||||
|
tc.Steps = append(tc.Steps, *currentStep)
|
||||||
|
currentStep = nil
|
||||||
|
default:
|
||||||
|
if currentStep != nil {
|
||||||
|
panic("cannot make request with step already in progress")
|
||||||
|
}
|
||||||
|
|
||||||
|
urlCopy := *req.URL
|
||||||
|
urlCopy.Host = "localhost:8080"
|
||||||
|
urlCopy.Scheme = "http"
|
||||||
|
proxiedReq, err := http.NewRequest(req.Method, urlCopy.String(), bytes.NewReader(body))
|
||||||
|
checkErr(err)
|
||||||
|
proxiedReq.Header = req.Header
|
||||||
|
resp, err := http.DefaultClient.Do(proxiedReq)
|
||||||
|
checkErr(err)
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
bodyOut, err := ioutil.ReadAll(resp.Body)
|
||||||
|
checkErr(err)
|
||||||
|
|
||||||
|
for k, vs := range resp.Header {
|
||||||
|
for _, v := range vs {
|
||||||
|
w.Header().Add(k, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
w.WriteHeader(resp.StatusCode)
|
||||||
|
w.Write(bodyOut)
|
||||||
|
|
||||||
|
if record {
|
||||||
|
infile := fmt.Sprintf("%d.request", len(tc.Steps))
|
||||||
|
outfile := fmt.Sprintf("%d.response", len(tc.Steps))
|
||||||
|
checkErr(ioutil.WriteFile(infile, tryIndent(body), os.FileMode(0755)))
|
||||||
|
checkErr(ioutil.WriteFile(outfile, tryIndent(bodyOut), os.FileMode(0755)))
|
||||||
|
tc.Steps = append(tc.Steps, EditStep{
|
||||||
|
StepType: "request",
|
||||||
|
Input: infile,
|
||||||
|
Output: outfile,
|
||||||
|
RequestContentType: req.Header.Get("Content-Type"),
|
||||||
|
RequestMethod: req.Method,
|
||||||
|
RequestPath: req.URL.Path,
|
||||||
|
ResponseStatusCode: resp.StatusCode,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tcData, err := yaml.Marshal(tc)
|
||||||
|
checkErr(err)
|
||||||
|
checkErr(ioutil.WriteFile("test.yaml", tcData, os.FileMode(0755)))
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkErr(err error) {
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func tryIndent(data []byte) []byte {
|
||||||
|
indented := &bytes.Buffer{}
|
||||||
|
if err := json.Indent(indented, data, "", "\t"); err == nil {
|
||||||
|
return indented.Bytes()
|
||||||
|
}
|
||||||
|
return data
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Copyright 2017 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.
|
||||||
|
|
||||||
|
set -o errexit
|
||||||
|
set -o nounset
|
||||||
|
set -o pipefail
|
||||||
|
|
||||||
|
# send the original content to the server
|
||||||
|
curl -s -k -XPOST "http://localhost:8081/callback/in" --data-binary "@${1}"
|
||||||
|
# allow the user to edit the file
|
||||||
|
vi "${1}"
|
||||||
|
# send the resulting content to the server
|
||||||
|
curl -s -k -XPOST "http://localhost:8081/callback/out" --data-binary "@${1}"
|
|
@ -0,0 +1,70 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Copyright 2017 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.
|
||||||
|
|
||||||
|
set -o errexit
|
||||||
|
set -o nounset
|
||||||
|
set -o pipefail
|
||||||
|
|
||||||
|
if [[ -z "${1-}" ]]; then
|
||||||
|
echo "Usage: record_testcase.sh testcase-name"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Clean up the test server
|
||||||
|
function cleanup {
|
||||||
|
if [[ ! -z "${pid-}" ]]; then
|
||||||
|
echo "Stopping recording server (${pid})"
|
||||||
|
# kill the process `go run` launched
|
||||||
|
pkill -P "${pid}"
|
||||||
|
# kill the `go run` process itself
|
||||||
|
kill -9 "${pid}"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
testcase="${1}"
|
||||||
|
|
||||||
|
test_root="$(dirname "${BASH_SOURCE}")"
|
||||||
|
testcase_dir="${test_root}/testcase-${testcase}"
|
||||||
|
mkdir -p "${testcase_dir}"
|
||||||
|
|
||||||
|
pushd "${testcase_dir}"
|
||||||
|
export EDITOR="../record_editor.sh"
|
||||||
|
go run "../record.go" &
|
||||||
|
pid=$!
|
||||||
|
trap cleanup EXIT
|
||||||
|
echo "Started recording server (${pid})"
|
||||||
|
|
||||||
|
# Make a kubeconfig that makes kubectl talk to our test server
|
||||||
|
edit_kubeconfig="${TMP:-/tmp}/edit_test.kubeconfig"
|
||||||
|
echo "apiVersion: v1
|
||||||
|
clusters:
|
||||||
|
- cluster:
|
||||||
|
server: http://localhost:8081
|
||||||
|
name: test
|
||||||
|
contexts:
|
||||||
|
- context:
|
||||||
|
cluster: test
|
||||||
|
user: test
|
||||||
|
name: test
|
||||||
|
current-context: test
|
||||||
|
kind: Config
|
||||||
|
users: []
|
||||||
|
" > "${edit_kubeconfig}"
|
||||||
|
export KUBECONFIG="${edit_kubeconfig}"
|
||||||
|
|
||||||
|
echo "Starting subshell. Type exit when finished."
|
||||||
|
bash
|
||||||
|
popd
|
|
@ -0,0 +1,32 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Copyright 2017 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.
|
||||||
|
|
||||||
|
set -o errexit
|
||||||
|
set -o nounset
|
||||||
|
set -o pipefail
|
||||||
|
|
||||||
|
# Send the file content to the server
|
||||||
|
if command -v curl &>/dev/null; then
|
||||||
|
curl -s -k -XPOST --data-binary "@${1}" -o "${1}.result" "${KUBE_EDITOR_CALLBACK}"
|
||||||
|
elif command -v wget &>/dev/null; then
|
||||||
|
wget --post-file="${1}" -O "${1}.result" "${KUBE_EDITOR_CALLBACK}"
|
||||||
|
else
|
||||||
|
echo "curl and wget are unavailable" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Use the response as the edited version
|
||||||
|
mv "${1}.result" "${1}"
|
|
@ -216,6 +216,9 @@ type TestFactory struct {
|
||||||
Namespace string
|
Namespace string
|
||||||
ClientConfig *restclient.Config
|
ClientConfig *restclient.Config
|
||||||
Err error
|
Err error
|
||||||
|
|
||||||
|
ClientForMappingFunc func(mapping *meta.RESTMapping) (resource.RESTClient, error)
|
||||||
|
UnstructuredClientForMappingFunc func(mapping *meta.RESTMapping) (resource.RESTClient, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type FakeFactory struct {
|
type FakeFactory struct {
|
||||||
|
@ -294,7 +297,10 @@ func (f *FakeFactory) BareClientConfig() (*restclient.Config, error) {
|
||||||
return f.tf.ClientConfig, f.tf.Err
|
return f.tf.ClientConfig, f.tf.Err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *FakeFactory) ClientForMapping(*meta.RESTMapping) (resource.RESTClient, error) {
|
func (f *FakeFactory) ClientForMapping(mapping *meta.RESTMapping) (resource.RESTClient, error) {
|
||||||
|
if f.tf.ClientForMappingFunc != nil {
|
||||||
|
return f.tf.ClientForMappingFunc(mapping)
|
||||||
|
}
|
||||||
return f.tf.Client, f.tf.Err
|
return f.tf.Client, f.tf.Err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -311,7 +317,10 @@ func (f *FakeFactory) ClientConfigForVersion(requiredVersion *schema.GroupVersio
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *FakeFactory) UnstructuredClientForMapping(*meta.RESTMapping) (resource.RESTClient, error) {
|
func (f *FakeFactory) UnstructuredClientForMapping(mapping *meta.RESTMapping) (resource.RESTClient, error) {
|
||||||
|
if f.tf.UnstructuredClientForMappingFunc != nil {
|
||||||
|
return f.tf.UnstructuredClientForMappingFunc(mapping)
|
||||||
|
}
|
||||||
return f.tf.UnstructuredClient, f.tf.Err
|
return f.tf.UnstructuredClient, f.tf.Err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -481,6 +490,9 @@ func (f *fakeMixedFactory) ClientForMapping(m *meta.RESTMapping) (resource.RESTC
|
||||||
if m.ObjectConvertor == api.Scheme {
|
if m.ObjectConvertor == api.Scheme {
|
||||||
return f.apiClient, f.tf.Err
|
return f.apiClient, f.tf.Err
|
||||||
}
|
}
|
||||||
|
if f.tf.ClientForMappingFunc != nil {
|
||||||
|
return f.tf.ClientForMappingFunc(m)
|
||||||
|
}
|
||||||
return f.tf.Client, f.tf.Err
|
return f.tf.Client, f.tf.Err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -553,11 +565,17 @@ func (f *fakeAPIFactory) ClientConfig() (*restclient.Config, error) {
|
||||||
return f.tf.ClientConfig, f.tf.Err
|
return f.tf.ClientConfig, f.tf.Err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *fakeAPIFactory) ClientForMapping(*meta.RESTMapping) (resource.RESTClient, error) {
|
func (f *fakeAPIFactory) ClientForMapping(m *meta.RESTMapping) (resource.RESTClient, error) {
|
||||||
|
if f.tf.ClientForMappingFunc != nil {
|
||||||
|
return f.tf.ClientForMappingFunc(m)
|
||||||
|
}
|
||||||
return f.tf.Client, f.tf.Err
|
return f.tf.Client, f.tf.Err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *fakeAPIFactory) UnstructuredClientForMapping(*meta.RESTMapping) (resource.RESTClient, error) {
|
func (f *fakeAPIFactory) UnstructuredClientForMapping(m *meta.RESTMapping) (resource.RESTClient, error) {
|
||||||
|
if f.tf.UnstructuredClientForMappingFunc != nil {
|
||||||
|
return f.tf.UnstructuredClientForMappingFunc(m)
|
||||||
|
}
|
||||||
return f.tf.UnstructuredClient, f.tf.Err
|
return f.tf.UnstructuredClient, f.tf.Err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -48,6 +48,7 @@ type RESTClient struct {
|
||||||
NegotiatedSerializer runtime.NegotiatedSerializer
|
NegotiatedSerializer runtime.NegotiatedSerializer
|
||||||
GroupName string
|
GroupName string
|
||||||
APIRegistry *registered.APIRegistrationManager
|
APIRegistry *registered.APIRegistrationManager
|
||||||
|
VersionedAPIPath string
|
||||||
|
|
||||||
Req *http.Request
|
Req *http.Request
|
||||||
Resp *http.Response
|
Resp *http.Response
|
||||||
|
@ -62,8 +63,8 @@ func (c *RESTClient) Put() *restclient.Request {
|
||||||
return c.request("PUT")
|
return c.request("PUT")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *RESTClient) Patch(_ types.PatchType) *restclient.Request {
|
func (c *RESTClient) Patch(pt types.PatchType) *restclient.Request {
|
||||||
return c.request("PATCH")
|
return c.request("PATCH").SetHeader("Content-Type", string(pt))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *RESTClient) Post() *restclient.Request {
|
func (c *RESTClient) Post() *restclient.Request {
|
||||||
|
@ -110,7 +111,7 @@ func (c *RESTClient) request(verb string) *restclient.Request {
|
||||||
serializers.StreamingSerializer = info.StreamSerializer.Serializer
|
serializers.StreamingSerializer = info.StreamSerializer.Serializer
|
||||||
serializers.Framer = info.StreamSerializer.Framer
|
serializers.Framer = info.StreamSerializer.Framer
|
||||||
}
|
}
|
||||||
return restclient.NewRequest(c, verb, &url.URL{Host: "localhost"}, "", config, serializers, nil, nil)
|
return restclient.NewRequest(c, verb, &url.URL{Host: "localhost"}, c.VersionedAPIPath, config, serializers, nil, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *RESTClient) Do(req *http.Request) (*http.Response, error) {
|
func (c *RESTClient) Do(req *http.Request) (*http.Response, error) {
|
||||||
|
|
Loading…
Reference in New Issue