github.com/spinnaker/spin@v1.30.0/cmd/canary/canary-config/retro.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 "errors" 19 "fmt" 20 "net/http" 21 "strings" 22 "time" 23 24 "github.com/antihax/optional" 25 "github.com/spf13/cobra" 26 27 gate "github.com/spinnaker/spin/gateapi" 28 "github.com/spinnaker/spin/util" 29 ) 30 31 type retroOptions struct { 32 *canaryConfigOptions 33 output string 34 canaryConfigFile string 35 controlGroup string 36 controlLocation string 37 experimentGroup string 38 experimentLocation string 39 startInstant string 40 endInstant string 41 extendedScopeParams map[string]string 42 metricsAccount string 43 storageAccount string 44 stepSize int 45 marginalScore int 46 passScore int 47 fullResult bool 48 } 49 50 const ( 51 retroTemplateShort = "Retro the provided canary config" 52 retroTemplateLong = "Retro the provided canary config" 53 ) 54 55 var retrySleepCycle = 6 * time.Second 56 57 func NewRetroCmd(canaryConfigOptions *canaryConfigOptions) *cobra.Command { 58 options := &retroOptions{ 59 canaryConfigOptions: canaryConfigOptions, 60 } 61 cmd := &cobra.Command{ 62 Use: "retro", 63 Aliases: []string{}, 64 Short: retroTemplateShort, 65 Long: retroTemplateLong, 66 RunE: func(cmd *cobra.Command, args []string) error { 67 return retroCanaryConfig(cmd, options) 68 }, 69 } 70 71 cmd.PersistentFlags().StringVarP(&options.canaryConfigFile, "file", 72 "f", "", "path to the canary config file") 73 cmd.PersistentFlags().StringVar(&options.controlGroup, "control-group", "", "Control server group name (required)") 74 cmd.PersistentFlags().StringVar(&options.controlLocation, "control-location", "", "Control server group location (required)") 75 cmd.PersistentFlags().StringVar(&options.experimentGroup, "experiment-group", "", "Experiment server group name (required)") 76 cmd.PersistentFlags().StringVar(&options.experimentLocation, "experiment-location", "", "Experiment server group location (required)") 77 cmd.PersistentFlags().StringVar(&options.startInstant, "start", "", "Start of canary window, in ISO Instant format (required)") 78 cmd.PersistentFlags().StringVar(&options.endInstant, "end", "", "End of canary window, in ISO Instant format (required)") 79 80 cmd.PersistentFlags().IntVar(&options.stepSize, "step", 10, "Canary sampling step size in seconds") 81 cmd.PersistentFlags().IntVar(&options.marginalScore, "marginal-score", 75, "Canary marginal score threshold") 82 cmd.PersistentFlags().IntVar(&options.passScore, "pass-score", 95, "Canary pass score threshold") 83 cmd.PersistentFlags().StringToStringVar(&options.extendedScopeParams, "extended-scope-params", nil, "Extended scope params for retrospective") 84 cmd.PersistentFlags().StringVar(&options.metricsAccount, "metrics-account", "", "Metrics account to use in the retrospective") 85 cmd.PersistentFlags().StringVar(&options.storageAccount, "storage-account", "", "Storage account to use in the retrospective") 86 87 cmd.PersistentFlags().BoolVar(&options.fullResult, "full-result", false, "Whether to print the full canary result") 88 89 return cmd 90 } 91 92 func retroCanaryConfig(cmd *cobra.Command, options *retroOptions) error { 93 canaryConfigJson, err := util.ParseJsonFromFileOrStdin(options.canaryConfigFile, false) 94 if err != nil { 95 return err 96 } 97 98 validateErr := validateOptions(options) 99 if validateErr != nil { 100 return validateErr 101 } 102 103 startTime, tErr := time.Parse(time.RFC3339, options.startInstant) 104 if tErr != nil { 105 return tErr 106 } 107 endTime, tErr := time.Parse(time.RFC3339, options.endInstant) 108 if tErr != nil { 109 return tErr 110 } 111 112 scopes := map[string]interface{}{ 113 "default": map[string]interface{}{ 114 "controlScope": map[string]interface{}{ 115 "scope": options.controlGroup, 116 "location": options.controlLocation, 117 "start": startTime, 118 "end": endTime, 119 "step": options.stepSize, 120 "extendedScopeParams": options.extendedScopeParams, 121 }, 122 "experimentScope": map[string]interface{}{ 123 "scope": options.experimentGroup, 124 "location": options.experimentLocation, 125 "start": startTime, 126 "end": endTime, 127 "step": options.stepSize, 128 "extendedScopeParams": options.extendedScopeParams, 129 }, 130 }, 131 } 132 133 executionRequest := map[string]interface{}{ 134 "scopes": scopes, 135 "thresholds": map[string]int{ 136 "pass": options.passScore, 137 "marginal": options.marginalScore, 138 }, 139 } 140 141 adhocRequest := map[string]interface{}{ 142 "canaryConfig": canaryConfigJson, 143 "executionRequest": executionRequest, 144 } 145 146 initiateOptionalParams := &gate.V2CanaryControllerApiInitiateCanaryWithConfigUsingPOSTOpts{} 147 if options.metricsAccount != "" { 148 initiateOptionalParams.MetricsAccountName = optional.NewString(options.metricsAccount) 149 } 150 if options.storageAccount != "" { 151 initiateOptionalParams.StorageAccountName = optional.NewString(options.storageAccount) 152 } 153 154 options.Ui.Info("Initiating canary execution for supplied canary config") 155 canaryExecutionResp, initiateResp, initiateErr := options.GateClient.V2CanaryControllerApi.InitiateCanaryWithConfigUsingPOST(options.GateClient.Context, adhocRequest, initiateOptionalParams) 156 157 if initiateErr != nil { 158 return initiateErr 159 } 160 161 if initiateResp.StatusCode != http.StatusOK { 162 return fmt.Errorf( 163 "Encountered an unexpected status code %d initiating execution for canary config\n", 164 initiateResp.StatusCode) 165 } 166 167 canaryExecutionId := canaryExecutionResp.(map[string]interface{})["canaryExecutionId"].(string) 168 options.Ui.Info(fmt.Sprintf("Spawned canary execution with id %s, polling for completion...", canaryExecutionId)) 169 170 queryOptionalParams := &gate.V2CanaryControllerApiGetCanaryResultUsingGET1Opts{} 171 if options.storageAccount != "" { 172 queryOptionalParams.StorageAccountName = optional.NewString(options.storageAccount) 173 } 174 175 canaryResult, canaryResultResp, canaryResultErr := options.GateClient.V2CanaryControllerApi.GetCanaryResultUsingGET1(options.GateClient.Context, canaryExecutionId, queryOptionalParams) 176 if canaryResultErr != nil { 177 return canaryResultErr 178 } 179 180 if canaryResultResp.StatusCode != http.StatusOK { 181 return fmt.Errorf( 182 "Encountered an unexpected status code %d querying canary execution with id: %s\n", 183 canaryResultResp.StatusCode, canaryExecutionId) 184 } 185 186 complete := canaryResult.(map[string]interface{})["complete"].(bool) 187 188 retries := 0 189 for retries < 10 && !complete && canaryResultErr == nil { 190 canaryResult, canaryResultResp, canaryResultErr = options.GateClient.V2CanaryControllerApi.GetCanaryResultUsingGET1(options.GateClient.Context, canaryExecutionId, queryOptionalParams) 191 complete = canaryResult.(map[string]interface{})["complete"].(bool) 192 time.Sleep(retrySleepCycle) 193 retries += 1 194 } 195 196 if canaryResultErr != nil { 197 return canaryResultErr 198 } 199 if !complete { 200 return fmt.Errorf( 201 "Canary execution %s incomplete after 60 seconds, aborting", canaryExecutionId) 202 } 203 204 judgement := canaryResult.(map[string]interface{})["result"].(map[string]interface{})["judgeResult"].(map[string]interface{})["score"].(map[string]interface{})["classification"].(string) 205 206 options.Ui.Info(fmt.Sprintf("Retrospective canary execution finished, judgement = %s", strings.ToUpper(judgement))) 207 if options.fullResult { 208 options.Ui.JsonOutput(canaryResult) 209 } 210 return nil 211 } 212 213 func validateOptions(options *retroOptions) error { 214 if options.controlGroup == "" || options.controlLocation == "" { 215 return errors.New("Required control group flags not supplied") 216 } 217 218 if options.experimentGroup == "" || options.experimentLocation == "" { 219 return errors.New("Required experiment group flags not supplied") 220 } 221 222 if options.startInstant == "" || options.endInstant == "" { 223 return errors.New("Required time interval flags not supplied") 224 } 225 return nil 226 }