github.com/spinnaker/spin@v1.30.0/cmd/canary/canary-config/save_test.go (about) 1 // Copyright (c) 2019, Waze, Inc. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package canary_config 16 17 import ( 18 "bytes" 19 "fmt" 20 "io" 21 "io/ioutil" 22 "net/http" 23 "net/http/httptest" 24 "os" 25 "strings" 26 "testing" 27 28 "github.com/spinnaker/spin/cmd" 29 "github.com/spinnaker/spin/cmd/canary" 30 "github.com/spinnaker/spin/util" 31 ) 32 33 func TestCanaryConfigSave_createjson(t *testing.T) { 34 saveBuffer := new(bytes.Buffer) 35 ts := testGateCanaryConfigSaveSuccess(saveBuffer) 36 defer ts.Close() 37 38 tempFile := tempCanaryConfigFile(testCanaryConfigJsonStr) 39 if tempFile == nil { 40 t.Fatal("Could not create temp canary config file.") 41 } 42 defer os.Remove(tempFile.Name()) 43 44 rootCmd, rootOpts := cmd.NewCmdRoot(ioutil.Discard, ioutil.Discard) 45 canaryCmd, canaryOpts := canary.NewCanaryCmd(rootOpts) 46 canaryCmd.AddCommand(NewCanaryConfigCmd(canaryOpts)) 47 rootCmd.AddCommand(canaryCmd) 48 49 args := []string{"canary", "canary-config", "save", "--file", tempFile.Name(), "--gate-endpoint", ts.URL} 50 rootCmd.SetArgs(args) 51 err := rootCmd.Execute() 52 if err != nil { 53 t.Fatalf("Command failed with: %s", err) 54 } 55 56 expected := strings.TrimSpace(testCanaryConfigJsonStr) 57 recieved := saveBuffer.Bytes() 58 util.TestPrettyJsonDiff(t, "save request body", expected, recieved) 59 } 60 61 func TestCanaryConfigSave_createyaml(t *testing.T) { 62 saveBuffer := new(bytes.Buffer) 63 ts := testGateCanaryConfigSaveSuccess(saveBuffer) 64 defer ts.Close() 65 66 tempFile := tempCanaryConfigFile(testCanaryConfigYamlStr) 67 if tempFile == nil { 68 t.Fatal("Could not create temp canary config file.") 69 } 70 defer os.Remove(tempFile.Name()) 71 72 rootCmd, rootOpts := cmd.NewCmdRoot(ioutil.Discard, ioutil.Discard) 73 canaryCmd, canaryOpts := canary.NewCanaryCmd(rootOpts) 74 canaryCmd.AddCommand(NewCanaryConfigCmd(canaryOpts)) 75 rootCmd.AddCommand(canaryCmd) 76 77 args := []string{"canary", "canary-config", "save", "--file", tempFile.Name(), "--gate-endpoint", ts.URL} 78 rootCmd.SetArgs(args) 79 err := rootCmd.Execute() 80 if err != nil { 81 t.Fatalf("Command failed with: %s", err) 82 } 83 84 expected := strings.TrimSpace(testCanaryConfigJsonStr) 85 recieved := saveBuffer.Bytes() 86 util.TestPrettyJsonDiff(t, "save request body", expected, recieved) 87 } 88 89 func TestCanaryConfigSave_update(t *testing.T) { 90 saveBuffer := new(bytes.Buffer) 91 ts := testGateCanaryConfigUpdateSuccess(saveBuffer) 92 defer ts.Close() 93 94 tempFile := tempCanaryConfigFile(testCanaryConfigJsonStr) 95 if tempFile == nil { 96 t.Fatal("Could not create temp canary config file.") 97 } 98 defer os.Remove(tempFile.Name()) 99 100 rootCmd, rootOpts := cmd.NewCmdRoot(ioutil.Discard, ioutil.Discard) 101 canaryCmd, canaryOpts := canary.NewCanaryCmd(rootOpts) 102 canaryCmd.AddCommand(NewCanaryConfigCmd(canaryOpts)) 103 rootCmd.AddCommand(canaryCmd) 104 105 args := []string{"canary", "canary-config", "save", "--file", tempFile.Name(), "--gate-endpoint", ts.URL} 106 rootCmd.SetArgs(args) 107 err := rootCmd.Execute() 108 if err != nil { 109 t.Fatalf("Command failed with: %s", err) 110 } 111 112 expected := strings.TrimSpace(testCanaryConfigJsonStr) 113 recieved := saveBuffer.Bytes() 114 util.TestPrettyJsonDiff(t, "save request body", expected, recieved) 115 } 116 117 func TestCanaryConfigSave_stdin(t *testing.T) { 118 saveBuffer := new(bytes.Buffer) 119 ts := testGateCanaryConfigUpdateSuccess(saveBuffer) 120 defer ts.Close() 121 122 tempFile := tempCanaryConfigFile(testCanaryConfigJsonStr) 123 if tempFile == nil { 124 t.Fatal("Could not create temp canary config file.") 125 } 126 defer os.Remove(tempFile.Name()) 127 128 // Prepare Stdin for test reading. 129 tempFile.Seek(0, 0) 130 oldStdin := os.Stdin 131 defer func() { os.Stdin = oldStdin }() 132 os.Stdin = tempFile 133 134 rootCmd, rootOpts := cmd.NewCmdRoot(ioutil.Discard, ioutil.Discard) 135 canaryCmd, canaryOpts := canary.NewCanaryCmd(rootOpts) 136 canaryCmd.AddCommand(NewCanaryConfigCmd(canaryOpts)) 137 rootCmd.AddCommand(canaryCmd) 138 139 args := []string{"canary", "canary-config", "save", "--gate-endpoint", ts.URL} 140 rootCmd.SetArgs(args) 141 err := rootCmd.Execute() 142 if err != nil { 143 t.Fatalf("Command failed with: %s", err) 144 } 145 146 expected := strings.TrimSpace(testCanaryConfigJsonStr) 147 recieved := saveBuffer.Bytes() 148 util.TestPrettyJsonDiff(t, "save request body", expected, recieved) 149 } 150 151 func TestCanaryConfigSave_fail(t *testing.T) { 152 ts := testGateFail() 153 defer ts.Close() 154 155 tempFile := tempCanaryConfigFile(testCanaryConfigJsonStr) 156 if tempFile == nil { 157 t.Fatal("Could not create temp canary config file.") 158 } 159 defer os.Remove(tempFile.Name()) 160 161 rootCmd, rootOpts := cmd.NewCmdRoot(ioutil.Discard, ioutil.Discard) 162 canaryCmd, canaryOpts := canary.NewCanaryCmd(rootOpts) 163 canaryCmd.AddCommand(NewCanaryConfigCmd(canaryOpts)) 164 rootCmd.AddCommand(canaryCmd) 165 166 args := []string{"canary", "canary-config", "save", "--file", tempFile.Name(), "--gate-endpoint", ts.URL} 167 rootCmd.SetArgs(args) 168 err := rootCmd.Execute() 169 if err == nil { 170 t.Fatalf("Command failed with: %s", err) 171 } 172 } 173 174 func TestCanaryConfigSave_flags(t *testing.T) { 175 saveBuffer := new(bytes.Buffer) 176 ts := testGateCanaryConfigUpdateSuccess(saveBuffer) 177 defer ts.Close() 178 179 rootCmd, rootOpts := cmd.NewCmdRoot(ioutil.Discard, ioutil.Discard) 180 canaryCmd, canaryOpts := canary.NewCanaryCmd(rootOpts) 181 canaryCmd.AddCommand(NewCanaryConfigCmd(canaryOpts)) 182 rootCmd.AddCommand(canaryCmd) 183 184 // Missing canary config spec file and stdin. 185 args := []string{"canary", "canary-config", "save", "--gate-endpoint", ts.URL} 186 rootCmd.SetArgs(args) 187 err := rootCmd.Execute() 188 if err == nil { 189 t.Fatalf("Command failed with: %s", err) 190 } 191 192 expected := "" 193 recieved := strings.TrimSpace(saveBuffer.String()) 194 if expected != recieved { 195 t.Fatalf("Unexpected save request body:\n%s", recieved) 196 } 197 } 198 199 func TestCanaryConfigSave_missingid(t *testing.T) { 200 saveBuffer := new(bytes.Buffer) 201 ts := testGateCanaryConfigUpdateSuccess(saveBuffer) 202 defer ts.Close() 203 204 tempFile := tempCanaryConfigFile(missingIdJsonStr) 205 if tempFile == nil { 206 t.Fatal("Could not create temp canary config file.") 207 } 208 defer os.Remove(tempFile.Name()) 209 210 rootCmd, rootOpts := cmd.NewCmdRoot(ioutil.Discard, ioutil.Discard) 211 canaryCmd, canaryOpts := canary.NewCanaryCmd(rootOpts) 212 canaryCmd.AddCommand(NewCanaryConfigCmd(canaryOpts)) 213 rootCmd.AddCommand(canaryCmd) 214 215 args := []string{"canary", "canary-config", "save", "--file", tempFile.Name(), "--gate-endpoint", ts.URL} 216 rootCmd.SetArgs(args) 217 err := rootCmd.Execute() 218 if err == nil { 219 t.Fatalf("Command failed with: %s", err) 220 } 221 222 expected := "" 223 recieved := strings.TrimSpace(saveBuffer.String()) 224 if expected != recieved { 225 t.Fatalf("Unexpected save request body:\n%s", recieved) 226 } 227 } 228 229 func tempCanaryConfigFile(canaryConfigContent string) *os.File { 230 tempFile, _ := ioutil.TempFile("" /* /tmp dir. */, "cc-spec") 231 bytes, err := tempFile.Write([]byte(canaryConfigContent)) 232 if err != nil || bytes == 0 { 233 fmt.Println("Could not write temp file.") 234 return nil 235 } 236 return tempFile 237 } 238 239 // testGateCanaryConfigUpdateSuccess spins up a local http server that we will configure the GateClient 240 // to direct requests to. Responds with OK to indicate a canary config exists, 241 // and Accepts POST calls. 242 // Writes request body to buffer for testing. 243 func testGateCanaryConfigUpdateSuccess(buffer io.Writer) *httptest.Server { 244 mux := util.TestGateMuxWithVersionHandler() 245 // Return that there are no existing CCs on GET and a successful id on PUT. 246 mux.Handle("/v2/canaryConfig/exampleCanaryConfigId", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 247 if r.Method == http.MethodPut { 248 defer r.Body.Close() 249 body, err := ioutil.ReadAll(r.Body) 250 if err != nil { 251 http.Error(w, "Failed to ready body", http.StatusInternalServerError) 252 return 253 } 254 buffer.Write([]byte(body)) 255 256 w.Write([]byte(responseJson)) 257 } else { 258 w.WriteHeader(http.StatusOK) 259 } 260 })) 261 return httptest.NewServer(mux) 262 } 263 264 // testGateCanaryConfigSaveSuccess spins up a local http server that we will configure the GateClient 265 // to direct requests to. Responds with 404 NotFound to indicate a canary config doesn't exist, 266 // and Accepts POST calls. 267 // Writes request body to buffer for testing. 268 func testGateCanaryConfigSaveSuccess(buffer io.Writer) *httptest.Server { 269 mux := util.TestGateMuxWithVersionHandler() 270 mux.Handle("/v2/canaryConfig", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 271 defer r.Body.Close() 272 body, err := ioutil.ReadAll(r.Body) 273 if err != nil { 274 http.Error(w, "Failed to ready body", http.StatusInternalServerError) 275 return 276 } 277 buffer.Write([]byte(body)) 278 279 w.Write([]byte(responseJson)) 280 })) 281 // Return that we found no CC to signal a create. 282 mux.Handle("/v2/canaryConfig/exampleCanaryConfigId", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 283 w.WriteHeader(http.StatusNotFound) 284 })) 285 return httptest.NewServer(mux) 286 } 287 288 const responseJson = ` 289 { 290 "id": "exampleCanaryConfigId" 291 } 292 ` 293 294 const missingIdJsonStr = ` 295 { 296 "applications": [ 297 "canaryconfigs" 298 ], 299 "classifier": { 300 "groupWeights": { 301 "Errors": 100 302 }, 303 "scoreThresholds": { 304 "marginal": 75, 305 "pass": 95 306 } 307 }, 308 "configVersion": "1", 309 "description": "Base canary config", 310 "judge": { 311 "judgeConfigurations": { }, 312 "name": "NetflixACAJudge-v1.0" 313 }, 314 "metrics": [ 315 { 316 "analysisConfigurations": { 317 "canary": { 318 "direction": "increase", 319 "nanStrategy": "replace" 320 } 321 }, 322 "groups": [ 323 "Errors" 324 ], 325 "name": "RequestFailureRate", 326 "query": { 327 "crossSeriesReducer": "REDUCE_SUM", 328 "customFilterTemplate": "ServiceGroupFilter", 329 "groupByFields": [ ], 330 "metricType": "custom.googleapis.com/server/failure_rate", 331 "perSeriesAligner": "ALIGN_MEAN", 332 "resourceType": "aws_ec2_instance", 333 "serviceType": "stackdriver", 334 "type": "stackdriver" 335 }, 336 "scopeName": "default" 337 } 338 ], 339 "name": "exampleCanary", 340 "templates": { 341 "ServiceGroupFilter": "metric.label.group_name = \"${scope}\"" 342 } 343 } 344 ` 345 346 const testCanaryConfigJsonStr = ` 347 { 348 "applications": [ 349 "canaryconfigs" 350 ], 351 "classifier": { 352 "groupWeights": { 353 "Errors": 100 354 }, 355 "scoreThresholds": { 356 "marginal": 75, 357 "pass": 95 358 } 359 }, 360 "configVersion": "1", 361 "description": "Base canary config", 362 "id": "exampleCanaryConfigId", 363 "judge": { 364 "judgeConfigurations": {}, 365 "name": "NetflixACAJudge-v1.0" 366 }, 367 "metrics": [ 368 { 369 "analysisConfigurations": { 370 "canary": { 371 "direction": "increase", 372 "nanStrategy": "replace" 373 } 374 }, 375 "groups": [ 376 "Errors" 377 ], 378 "name": "RequestFailureRate", 379 "query": { 380 "crossSeriesReducer": "REDUCE_SUM", 381 "customFilterTemplate": "ServiceGroupFilter", 382 "groupByFields": [], 383 "metricType": "custom.googleapis.com/server/failure_rate", 384 "perSeriesAligner": "ALIGN_MEAN", 385 "resourceType": "aws_ec2_instance", 386 "serviceType": "stackdriver", 387 "type": "stackdriver" 388 }, 389 "scopeName": "default" 390 } 391 ], 392 "name": "exampleCanary", 393 "templates": { 394 "ServiceGroupFilter": "metric.label.group_name = \"${scope}\"" 395 } 396 } 397 ` 398 399 const testCanaryConfigYamlStr = ` 400 applications: 401 - canaryconfigs 402 classifier: 403 groupWeights: 404 Errors: 100 405 scoreThresholds: 406 marginal: 75 407 pass: 95 408 configVersion: '1' 409 description: Base canary config 410 id: exampleCanaryConfigId 411 judge: 412 judgeConfigurations: {} 413 name: NetflixACAJudge-v1.0 414 metrics: 415 - analysisConfigurations: 416 canary: 417 direction: increase 418 nanStrategy: replace 419 groups: 420 - Errors 421 name: RequestFailureRate 422 query: 423 crossSeriesReducer: REDUCE_SUM 424 customFilterTemplate: ServiceGroupFilter 425 groupByFields: [] 426 metricType: custom.googleapis.com/server/failure_rate 427 perSeriesAligner: ALIGN_MEAN 428 resourceType: aws_ec2_instance 429 serviceType: stackdriver 430 type: stackdriver 431 scopeName: default 432 name: exampleCanary 433 templates: 434 ServiceGroupFilter: metric.label.group_name = "${scope}" 435 `