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  `