1
16
17 package edit
18
19 import (
20 "bytes"
21 "encoding/json"
22 "io"
23 "net/http"
24 "net/http/httptest"
25 "os"
26 "path/filepath"
27 "strings"
28 "testing"
29
30 "github.com/google/go-cmp/cmp"
31 "github.com/spf13/cobra"
32
33 yaml "gopkg.in/yaml.v2"
34
35 "k8s.io/apimachinery/pkg/runtime/schema"
36 "k8s.io/apimachinery/pkg/util/sets"
37 "k8s.io/cli-runtime/pkg/genericiooptions"
38 "k8s.io/cli-runtime/pkg/resource"
39 "k8s.io/client-go/rest/fake"
40 "k8s.io/kubectl/pkg/cmd/apply"
41 "k8s.io/kubectl/pkg/cmd/create"
42 cmdtesting "k8s.io/kubectl/pkg/cmd/testing"
43 cmdutil "k8s.io/kubectl/pkg/cmd/util"
44 )
45
46 type EditTestCase struct {
47 Description string `yaml:"description"`
48
49 Mode string `yaml:"mode"`
50 Args []string `yaml:"args"`
51 Filename string `yaml:"filename"`
52 Output string `yaml:"outputFormat"`
53 OutputPatch string `yaml:"outputPatch"`
54 SaveConfig string `yaml:"saveConfig"`
55 Subresource string `yaml:"subresource"`
56 Namespace string `yaml:"namespace"`
57 ExpectedStdout []string `yaml:"expectedStdout"`
58 ExpectedStderr []string `yaml:"expectedStderr"`
59 ExpectedExitCode int `yaml:"expectedExitCode"`
60
61 Steps []EditStep `yaml:"steps"`
62 }
63
64 type EditStep struct {
65
66 StepType string `yaml:"type"`
67
68
69 RequestMethod string `yaml:"expectedMethod,omitempty"`
70 RequestPath string `yaml:"expectedPath,omitempty"`
71 RequestContentType string `yaml:"expectedContentType,omitempty"`
72 Input string `yaml:"expectedInput"`
73
74
75 ResponseStatusCode int `yaml:"resultingStatusCode,omitempty"`
76
77 Output string `yaml:"resultingOutput"`
78 }
79
80 func TestEdit(t *testing.T) {
81 var (
82 name string
83 testcase EditTestCase
84 i int
85 err error
86 )
87
88 const updateEnvVar = "UPDATE_EDIT_FIXTURE_DATA"
89 updateInputFixtures := os.Getenv(updateEnvVar) == "true"
90
91 reqResp := func(req *http.Request) (*http.Response, error) {
92 defer func() { i++ }()
93 if i > len(testcase.Steps)-1 {
94 t.Fatalf("%s, step %d: more requests than steps, got %s %s", name, i, req.Method, req.URL.Path)
95 }
96 step := testcase.Steps[i]
97
98 body := []byte{}
99 if req.Body != nil {
100 body, err = io.ReadAll(req.Body)
101 if err != nil {
102 t.Fatalf("%s, step %d: %v", name, i, err)
103 }
104 }
105
106 inputFile := filepath.Join("testdata", "testcase-"+name, step.Input)
107 expectedInput, err := os.ReadFile(inputFile)
108 if err != nil {
109 t.Fatalf("%s, step %d: %v", name, i, err)
110 }
111
112 outputFile := filepath.Join("testdata", "testcase-"+name, step.Output)
113 resultingOutput, err := os.ReadFile(outputFile)
114 if err != nil {
115 t.Fatalf("%s, step %d: %v", name, i, err)
116 }
117
118 if req.Method == "POST" && req.URL.Path == "/callback" {
119 if step.StepType != "edit" {
120 t.Fatalf("%s, step %d: expected edit step, got %s %s", name, i, req.Method, req.URL.Path)
121 }
122 if !bytes.Equal(body, expectedInput) {
123 if updateInputFixtures {
124
125 os.WriteFile(inputFile, body, os.FileMode(0644))
126 } else {
127 t.Errorf("%s, step %d: diff in edit content:\n%s", name, i, cmp.Diff(string(body), string(expectedInput)))
128 t.Logf("If the change in input is expected, rerun tests with %s=true to update input fixtures", updateEnvVar)
129 }
130 }
131 return &http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(bytes.NewReader(resultingOutput))}, nil
132 }
133 if step.StepType != "request" {
134 t.Fatalf("%s, step %d: expected request step, got %s %s", name, i, req.Method, req.URL.Path)
135 }
136 body = tryIndent(body)
137 expectedInput = tryIndent(expectedInput)
138 if req.Method != step.RequestMethod || req.URL.Path != step.RequestPath || req.Header.Get("Content-Type") != step.RequestContentType {
139 t.Fatalf(
140 "%s, step %d: expected \n%s %s (content-type=%s)\ngot\n%s %s (content-type=%s)", name, i,
141 step.RequestMethod, step.RequestPath, step.RequestContentType,
142 req.Method, req.URL.Path, req.Header.Get("Content-Type"),
143 )
144 }
145 if !bytes.Equal(body, expectedInput) {
146 if updateInputFixtures {
147
148 os.WriteFile(inputFile, body, os.FileMode(0644))
149 } else {
150 t.Errorf("%s, step %d: diff in edit content:\n%s", name, i, cmp.Diff(string(body), string(expectedInput)))
151 t.Logf("If the change in input is expected, rerun tests with %s=true to update input fixtures", updateEnvVar)
152 }
153 }
154 return &http.Response{StatusCode: step.ResponseStatusCode, Header: cmdtesting.DefaultHeader(), Body: io.NopCloser(bytes.NewReader(resultingOutput))}, nil
155
156 }
157
158 handler := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
159 resp, _ := reqResp(req)
160 for k, vs := range resp.Header {
161 w.Header().Del(k)
162 for _, v := range vs {
163 w.Header().Add(k, v)
164 }
165 }
166 w.WriteHeader(resp.StatusCode)
167 io.Copy(w, resp.Body)
168 })
169
170 server := httptest.NewServer(handler)
171 defer server.Close()
172
173 t.Setenv("KUBE_EDITOR", "testdata/test_editor.sh")
174 t.Setenv("KUBE_EDITOR_CALLBACK", server.URL+"/callback")
175
176 testcases := sets.NewString()
177 filepath.Walk("testdata", func(path string, info os.FileInfo, err error) error {
178 if err != nil {
179 return err
180 }
181 if path == "testdata" {
182 return nil
183 }
184 name := filepath.Base(path)
185 if info.IsDir() {
186 if strings.HasPrefix(name, "testcase-") {
187 testcases.Insert(strings.TrimPrefix(name, "testcase-"))
188 }
189 return filepath.SkipDir
190 }
191 return nil
192 })
193
194 if !testcases.Has("create-list") {
195 t.Fatalf("Error locating edit testcases")
196 }
197
198 for _, testcaseName := range testcases.List() {
199 t.Run(testcaseName, func(t *testing.T) {
200 i = 0
201 name = testcaseName
202 testcase = EditTestCase{}
203 testcaseDir := filepath.Join("testdata", "testcase-"+name)
204 testcaseData, err := os.ReadFile(filepath.Join(testcaseDir, "test.yaml"))
205 if err != nil {
206 t.Fatalf("%s: %v", name, err)
207 }
208 if err := yaml.Unmarshal(testcaseData, &testcase); err != nil {
209 t.Fatalf("%s: %v", name, err)
210 }
211
212 tf := cmdtesting.NewTestFactory()
213 defer tf.Cleanup()
214
215 tf.UnstructuredClientForMappingFunc = func(gv schema.GroupVersion) (resource.RESTClient, error) {
216 versionedAPIPath := ""
217 if gv.Group == "" {
218 versionedAPIPath = "/api/" + gv.Version
219 } else {
220 versionedAPIPath = "/apis/" + gv.Group + "/" + gv.Version
221 }
222 return &fake.RESTClient{
223 VersionedAPIPath: versionedAPIPath,
224 NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer,
225 Client: fake.CreateHTTPClient(reqResp),
226 }, nil
227 }
228 tf.WithNamespace(testcase.Namespace)
229 tf.ClientConfigVal = cmdtesting.DefaultClientConfig()
230 ioStreams, _, buf, errBuf := genericiooptions.NewTestIOStreams()
231
232 var cmd *cobra.Command
233 switch testcase.Mode {
234 case "edit":
235 cmd = NewCmdEdit(tf, ioStreams)
236 case "create":
237 cmd = create.NewCmdCreate(tf, ioStreams)
238 cmd.Flags().Set("edit", "true")
239 case "edit-last-applied":
240 cmd = apply.NewCmdApplyEditLastApplied(tf, ioStreams)
241 default:
242 t.Fatalf("%s: unexpected mode %s", name, testcase.Mode)
243 }
244 if len(testcase.Filename) > 0 {
245 cmd.Flags().Set("filename", filepath.Join(testcaseDir, testcase.Filename))
246 }
247 if len(testcase.Output) > 0 {
248 cmd.Flags().Set("output", testcase.Output)
249 }
250 if len(testcase.OutputPatch) > 0 {
251 cmd.Flags().Set("output-patch", testcase.OutputPatch)
252 }
253 if len(testcase.SaveConfig) > 0 {
254 cmd.Flags().Set("save-config", testcase.SaveConfig)
255 }
256 if len(testcase.Subresource) > 0 {
257 cmd.Flags().Set("subresource", testcase.Subresource)
258 }
259
260 cmdutil.BehaviorOnFatal(func(str string, code int) {
261 errBuf.WriteString(str)
262 if testcase.ExpectedExitCode != code {
263 t.Errorf("%s: expected exit code %d, got %d: %s", name, testcase.ExpectedExitCode, code, str)
264 }
265 })
266
267 cmd.Run(cmd, testcase.Args)
268
269 stdout := buf.String()
270 stderr := errBuf.String()
271
272 for _, s := range testcase.ExpectedStdout {
273 if !strings.Contains(stdout, s) {
274 t.Errorf("%s: expected to see '%s' in stdout\n\nstdout:\n%s\n\nstderr:\n%s", name, s, stdout, stderr)
275 }
276 }
277 for _, s := range testcase.ExpectedStderr {
278 if !strings.Contains(stderr, s) {
279 t.Errorf("%s: expected to see '%s' in stderr\n\nstdout:\n%s\n\nstderr:\n%s", name, s, stdout, stderr)
280 }
281 }
282 if i < len(testcase.Steps) {
283 t.Errorf("%s: saw %d steps, testcase included %d additional steps that were not exercised", name, i, len(testcase.Steps)-i)
284 }
285 })
286 }
287 }
288
289 func tryIndent(data []byte) []byte {
290 indented := &bytes.Buffer{}
291 if err := json.Indent(indented, data, "", "\t"); err == nil {
292 return indented.Bytes()
293 }
294 return data
295 }
296
View as plain text