From 757304c21ceafa4f7d6b7ed529410b540da127dd Mon Sep 17 00:00:00 2001 From: Prasanna Krishna <107837335+NSSPKrishna@users.noreply.github.com> Date: Wed, 30 Aug 2023 17:25:03 +0530 Subject: [PATCH] feat(synthetics): addition of a command to run automated tests on monitors (#1508) Co-authored-by: pranav-new-relic Co-authored-by: pranav-new-relic <127438038+pranav-new-relic@users.noreply.github.com> --- go.mod | 10 +- go.sum | 19 +- internal/output/text.go | 48 ++++ .../automated_tests_utilities_yaml.go | 19 ++ internal/synthetics/command_batch_run.go | 221 ++++++++++++++++++ internal/synthetics/command_monitor_test.go | 10 +- 6 files changed, 311 insertions(+), 16 deletions(-) create mode 100644 internal/synthetics/automated_tests_utilities_yaml.go create mode 100644 internal/synthetics/command_batch_run.go diff --git a/go.mod b/go.mod index cac912c92..4c09840b4 100644 --- a/go.mod +++ b/go.mod @@ -16,11 +16,11 @@ require ( github.com/jedib0t/go-pretty/v6 v6.4.4 github.com/joshdk/go-junit v0.0.0-20210226021600-6145f504ca0d github.com/mitchellh/go-homedir v1.1.0 - github.com/newrelic/newrelic-client-go/v2 v2.20.0 + github.com/newrelic/newrelic-client-go/v2 v2.21.0 github.com/pkg/errors v0.9.1 github.com/shirou/gopsutil/v3 v3.22.12 github.com/sirupsen/logrus v1.9.0 - github.com/spf13/cobra v1.6.1 + github.com/spf13/cobra v1.7.0 github.com/spf13/pflag v1.0.5 github.com/splitio/go-client/v6 v6.2.1 github.com/stretchr/testify v1.8.1 @@ -48,21 +48,21 @@ require ( github.com/google/go-querystring v1.1.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.1 // indirect github.com/hashicorp/go-retryablehttp v0.7.0 // indirect - github.com/inconshreveable/mousetrap v1.0.1 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/itchyny/timefmt-go v0.1.5 // indirect github.com/joho/godotenv v1.4.0 // indirect github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.17 // indirect - github.com/mattn/go-runewidth v0.0.14 // indirect + github.com/mattn/go-runewidth v0.0.15 // indirect github.com/mattn/go-zglob v0.0.3 // indirect github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect github.com/radovskyb/watcher v1.0.7 // indirect - github.com/rivo/uniseg v0.2.0 // indirect + github.com/rivo/uniseg v0.4.4 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/segmentio/backo-go v1.0.1 // indirect github.com/splitio/go-split-commons/v4 v4.2.0 // indirect diff --git a/go.sum b/go.sum index 9ffc2ddf1..6f0d6618f 100644 --- a/go.sum +++ b/go.sum @@ -85,8 +85,8 @@ github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpO github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk= github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg= -github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc= -github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/itchyny/gojq v0.12.11 h1:YhLueoHhHiN4mkfM+3AyJV6EPcCxKZsOnYf+aVSwaQw= github.com/itchyny/gojq v0.12.11/go.mod h1:o3FT8Gkbg/geT4pLI0tF3hvip5F3Y/uskjRz9OYa38g= github.com/itchyny/timefmt-go v0.1.5 h1:G0INE2la8S6ru/ZI5JecgyzbbJNs5lG1RcBqa7Jm6GE= @@ -120,8 +120,8 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/ github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= -github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= -github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= +github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-zglob v0.0.3 h1:6Ry4EYsScDyt5di4OI6xw1bYhOqfE5S33Z1OPy+d+To= github.com/mattn/go-zglob v0.0.3/go.mod h1:9fxibJccNxU2cnpIKLRRFA7zX7qhkJIQWBb449FYHOo= github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4= @@ -131,8 +131,8 @@ github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrk github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4= github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE= github.com/mitchellh/mapstructure v1.4.3 h1:OVowDSCllw/YjdLkam3/sm7wEtOy59d8ndGgCcyj8cs= -github.com/newrelic/newrelic-client-go/v2 v2.20.0 h1:87aHvOCnv8MeZL6pm4VptjZKFxatmQq//35onHGBVic= -github.com/newrelic/newrelic-client-go/v2 v2.20.0/go.mod h1:VPWTvEfKvnTZLunAC7fiW33y4e0srznNfN5HJH2cOp8= +github.com/newrelic/newrelic-client-go/v2 v2.21.0 h1:SZ6FEwbLG7nzCJaT402dWPSDvqVegBoCEX7XsAEd3y8= +github.com/newrelic/newrelic-client-go/v2 v2.21.0/go.mod h1:VPWTvEfKvnTZLunAC7fiW33y4e0srznNfN5HJH2cOp8= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= @@ -157,8 +157,9 @@ github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/radovskyb/watcher v1.0.7 h1:AYePLih6dpmS32vlHfhCeli8127LzkIgwJGcwwe8tUE= github.com/radovskyb/watcher v1.0.7/go.mod h1:78okwvY5wPdzcb1UYnip1pvrZNIVEIh/Cm+ZuvsUYIg= -github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= +github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.8.1-0.20210923151022-86f73c517451 h1:d1PiN4RxzIFXCJTvRkvSkKqwtRAl5ZV4lATKtQI0B7I= github.com/rogpeppe/go-internal v1.8.1-0.20210923151022-86f73c517451/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o= @@ -170,8 +171,8 @@ github.com/shirou/gopsutil/v3 v3.22.12 h1:oG0ns6poeUSxf78JtOsfygNWuEHYYz8hnnNg7P github.com/shirou/gopsutil/v3 v3.22.12/go.mod h1:Xd7P1kwZcp5VW52+9XsirIKd/BROzbb2wdX3Kqlz9uI= github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA= -github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY= +github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= +github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/splitio/go-client/v6 v6.2.1 h1:EH3xYH7fr2c0I0ZtYvsyn7DjC9ZmoNAFLoKoT3BmQFU= diff --git a/internal/output/text.go b/internal/output/text.go index d3d8eeba4..050dca95c 100644 --- a/internal/output/text.go +++ b/internal/output/text.go @@ -218,3 +218,51 @@ func (o *Output) newTableWriter() table.Writer { return t } + +func (o *Output) syntheticNewTableWriter() table.Writer { + t := table.NewWriter() + t.SetOutputMirror(os.Stdout) + t.SetAllowedRowLength(o.terminalWidth) + + t.SetStyle(table.Style{ + Name: "nr-syn-cli-table", + Box: table.StyleBoxRounded, + Color: table.ColorOptions{ + Header: text.Colors{text.Bold}, + }, + Options: table.Options{ + DrawBorder: true, + SeparateColumns: true, + SeparateHeader: true, + }, + }) + + return t +} + +// PrintResultTable prints the New Relic Synthetic Atuomated tests +// in a tabular format by default +func PrintResultTable(tableData [][]string) { + o := &Output{terminalWidth: 200} + + tw := o.syntheticNewTableWriter() + + // Add the header + tw.AppendHeader(table.Row{"Status", "Monitor Name", "Monitor GUID", "isBlocking"}) + + // Add the rows + for _, row := range tableData { + tw.AppendRow(stringSliceToRow(row)) + } + + // Render the table + tw.Render() +} + +func stringSliceToRow(slice []string) table.Row { + row := make(table.Row, len(slice)) + for i, v := range slice { + row[i] = v + } + return row +} diff --git a/internal/synthetics/automated_tests_utilities_yaml.go b/internal/synthetics/automated_tests_utilities_yaml.go new file mode 100644 index 000000000..253856714 --- /dev/null +++ b/internal/synthetics/automated_tests_utilities_yaml.go @@ -0,0 +1,19 @@ +package synthetics + +import "github.com/newrelic/newrelic-client-go/v2/pkg/synthetics" + +// a wrapper structure for the input to be sent to syntheticsStartAutomatedTest +type StartAutomatedTestInput struct { + Config synthetics.SyntheticsAutomatedTestConfigInput `json:"config,omitempty"` + Tests []synthetics.SyntheticsAutomatedTestMonitorInput `json:"tests,omitempty"` +} + +var globalResultExitCodes = map[synthetics.SyntheticsAutomatedTestStatus]*int{ + synthetics.SyntheticsAutomatedTestStatusTypes.FAILED: intPtr(1), + synthetics.SyntheticsAutomatedTestStatusTypes.PASSED: intPtr(0), + synthetics.SyntheticsAutomatedTestStatusTypes.TIMEOUT: intPtr(3), +} + +func intPtr(value int) *int { + return &value +} diff --git a/internal/synthetics/command_batch_run.go b/internal/synthetics/command_batch_run.go new file mode 100644 index 000000000..5e38775c4 --- /dev/null +++ b/internal/synthetics/command_batch_run.go @@ -0,0 +1,221 @@ +package synthetics + +import ( + "fmt" + "os" + "time" + + "github.com/newrelic/newrelic-cli/internal/client" + configAPI "github.com/newrelic/newrelic-cli/internal/config/api" + "github.com/newrelic/newrelic-cli/internal/install/ux" + "github.com/newrelic/newrelic-cli/internal/output" + "github.com/newrelic/newrelic-cli/internal/utils" + "github.com/newrelic/newrelic-client-go/v2/pkg/synthetics" + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + "gopkg.in/yaml.v3" +) + +var ( + batchFile string + guid []string + pollingInterval = time.Second * 30 + progressIndicator = ux.NewSpinner() + nrdbLatency = time.Second * 5 +) + +var cmdRun = &cobra.Command{ + Use: "run", + Example: "newrelic synthetics run --batchFile filename.yml", + Short: "Start an automated testing job on a batch of synthetic monitors", + Long: `Start an automated testing job on a batch of synthetic monitors + +The run command helps start an automated testing job by creating a batch, comprising the specified monitors and their +specifications (such as overrides), and subsequently, keeps fetching the status of the batch at periodic intervals of +time, until the status of the batch, which reflects the consolidated status of all monitors in the batch, is either +success, failure or timed out. + +The command may be used with the following flags (the arguments --batchFile and --guid are mutually exclusive). + +newrelic synthetics run --batchFile filename.yml +newrelic synthetics run --guid --guid +`, + Run: func(cmd *cobra.Command, args []string) { + var ( + config StartAutomatedTestInput + err error + testsBatchID string + ) + accountID := configAPI.GetActiveProfileAccountID() + if batchFile != "" || len(guid) != 0 { + config, err = parseConfiguration() + if err != nil { + log.Fatal(err) + } + + testsBatchID = createAutomatedTestBatch(config) + output.Printf("Generated Batch ID: %s", testsBatchID) + + // can be ignored if there is no initial tick by the ticker + time.Sleep(nrdbLatency) + getAutomatedTestResults(accountID, testsBatchID) + + } else { + utils.LogIfError(cmd.Help()) + } + + }, +} + +// Definition of the command +func init() { + cmdRun.Flags().StringVarP(&batchFile, "batchFile", "b", "", "Path to the YAML file comprising GUIDs of monitors and associated configuration") + cmdRun.Flags().StringSliceVarP(&guid, "guid", "g", nil, "List of GUIDs of monitors to include in the batch and run automated tests on") + Command.AddCommand(cmdRun) + + // MarkFlagsMutuallyExclusive allows one flag at once be invoked + cmdRun.MarkFlagsMutuallyExclusive("batchFile", "guid") +} + +// parseConfiguration helps parse the inputs given to this command, based on the format specified (YAML or command line GUIDs) +func parseConfiguration() (StartAutomatedTestInput, error) { + if batchFile != "" { + return createConfigurationUsingYAML(batchFile) + } else if len(guid) != 0 { + return createConfigurationUsingGUIDs(guid), nil + } + return StartAutomatedTestInput{}, fmt.Errorf("invalid arguments") +} + +// createConfigurationUsingYAML unmarshals the specified YAML file into an object that can be used +// to send a create batch request to NerdGraph +func createConfigurationUsingYAML(batchFile string) (StartAutomatedTestInput, error) { + var config StartAutomatedTestInput + + content, err := os.ReadFile(batchFile) + if err != nil { + return config, err + } + err = yaml.Unmarshal(content, &config) + if err != nil { + return config, err + } + + utils.LogIfFatal(err) + return config, nil +} + +// createConfigurationUsingGUIDs obtains GUIDs specified in command line arguments and restructures them into an object +// that can be used to send a create batch request to NerdGraph +func createConfigurationUsingGUIDs(guids []string) StartAutomatedTestInput { + var tests []synthetics.SyntheticsAutomatedTestMonitorInput + for _, id := range guids { + tests = append(tests, synthetics.SyntheticsAutomatedTestMonitorInput{ + MonitorGUID: synthetics.EntityGUID(id), + }) + } + + return StartAutomatedTestInput{ + Tests: tests, + } +} + +// createAutomatedTestBatch performs an API call to create a batch with the specified configuration and tests +func createAutomatedTestBatch(config StartAutomatedTestInput) string { + if len(config.Tests) == 0 { + log.Fatal("No valid monitors found in the input specified. Please check the input provided.") + } + progressIndicator.Start("Sending a request to create a batch with the specified monitors...") + + result, err := client.NRClient.Synthetics.SyntheticsStartAutomatedTest(config.Config, config.Tests) + progressIndicator.Stop() + if err != nil { + utils.LogIfFatal(err) + } + + return result.BatchId +} + +// getAutomatedTestResults performs an API call at regular intervals of time (when the pollingInterval has elapsed) +// to fetch the consolidated status of the batch, and the results of monitors the batch comprises +func getAutomatedTestResults(accountID int, testsBatchID string) { + // An infinite loop + ticker := time.NewTicker(pollingInterval) + defer ticker.Stop() + + for progressIndicator.Start("Fetching the status of tests in the batch...."); true; <-ticker.C { + batchResult, err := client.NRClient.Synthetics.GetAutomatedTestResult(accountID, testsBatchID) + progressIndicator.Stop() + + if err != nil { + log.Fatal(err) + } + + exitStatus, ok := globalResultExitCodes[(batchResult.Status)] + if !ok { + if batchResult.Status != synthetics.SyntheticsAutomatedTestStatusTypes.IN_PROGRESS { + log.Fatal("Unknown Error") + } + } + + renderMonitorTestsSummary(*batchResult, exitStatus) + + // Force flush the standard output buffer + os.Stdout.Sync() + + // exit, if the status is not IN_PROGRESS + if batchResult.Status != synthetics.SyntheticsAutomatedTestStatusTypes.IN_PROGRESS { + os.Exit(*exitStatus) + } + progressIndicator.Start("Fetching the status of tests in the batch....") + } +} + +// renderMonitorTestsSummary reads through the results of monitors fetched, restructures and renders these results accordingly +func renderMonitorTestsSummary(batchResult synthetics.SyntheticsAutomatedTestResult, exitStatus *int) { + fmt.Println("Status: ", batchResult.Status, " ") + summary, tableData := getMonitorTestsSummary(batchResult) + fmt.Printf("Summary: %s\n", summary) + if len(tableData) > 0 { + output.PrintResultTable(tableData) + } + + if batchResult.Status != synthetics.SyntheticsAutomatedTestStatusTypes.IN_PROGRESS { + exitStatusMessage := fmt.Sprintf("Exit Status: %d\n", *exitStatus) + fmt.Println(exitStatusMessage) + } +} + +// getMonitorTestsSummary reads through the results of monitors fetched and populates them to a table with details +// of each monitor, to print these results to the terminal +func getMonitorTestsSummary(batchResult synthetics.SyntheticsAutomatedTestResult) (string, [][]string) { + results := map[string][]synthetics.SyntheticsAutomatedTestJobResult{} + + for _, test := range batchResult.Tests { + if test.Result == "" { + test.Result = synthetics.SyntheticsJobStatusTypes.PENDING + } + + results[string(test.Result)] = append(results[string(test.Result)], test) + } + + summaryMessage := fmt.Sprintf("%d succeeded; %d failed; %d in progress.", + len(results[string(synthetics.SyntheticsJobStatusTypes.SUCCESS)]), + len(results[string(synthetics.SyntheticsJobStatusTypes.FAILED)]), + len(results[string(synthetics.SyntheticsJobStatusTypes.PENDING)])) + + tableData := make([][]string, 0) + + for status, tests := range results { + for _, test := range tests { + tableData = append(tableData, []string{ + status, + test.MonitorName, + string(test.MonitorGUID), + fmt.Sprintf("%t", test.AutomatedTestMonitorConfig.IsBlocking), + }) + } + } + + return summaryMessage, tableData +} diff --git a/internal/synthetics/command_monitor_test.go b/internal/synthetics/command_monitor_test.go index 8862c9be6..4bf9b2c33 100644 --- a/internal/synthetics/command_monitor_test.go +++ b/internal/synthetics/command_monitor_test.go @@ -6,9 +6,8 @@ package synthetics import ( "testing" - "github.com/stretchr/testify/assert" - "github.com/newrelic/newrelic-cli/internal/testcobra" + "github.com/stretchr/testify/assert" ) func TestSyntheticsMonitor(t *testing.T) { @@ -38,3 +37,10 @@ func TestSyntheticsMonitorList(t *testing.T) { testcobra.CheckCobraMetadata(t, cmdMonList) testcobra.CheckCobraRequiredFlags(t, cmdMonList, []string{}) } + +func TestSyntheticsMonitorRun(t *testing.T) { + assert.Equal(t, "run", cmdRun.Name()) + + testcobra.CheckCobraMetadata(t, cmdRun) + testcobra.CheckCobraRequiredFlags(t, cmdRun, []string{}) +}