From f7e48aff7828abe8f565b367ed474a08da1f0d0a Mon Sep 17 00:00:00 2001 From: Daniel Furman Date: Tue, 28 Jun 2022 17:53:56 +0200 Subject: [PATCH] Provide consistent interface for Synthetics Tests API (#103) * Provide consistent interface for Synthetics Tests API GetTestResults() and GetTestTrace() methods will be implemented in next PR. Issue: KNTK-399 --- README.md | 4 +- examples/cloud_export_example_test.go | 7 +- examples/synthetics_example_test.go | 587 ++++++-- kentikapi/internal/cloud/cloud.go | 4 +- kentikapi/internal/cloud/export.go | 4 +- kentikapi/internal/convert/convert.go | 53 + kentikapi/internal/convert/convert_test.go | 34 + kentikapi/internal/synthetics/synthetics.go | 129 +- kentikapi/internal/synthetics/test.go | 664 +++++++++ kentikapi/synth_spy_server_test.go | 160 +- kentikapi/synth_test_integration_test.go | 1453 +++++++++++++++++++ kentikapi/synthetics/agent.go | 2 +- kentikapi/synthetics/test.go | 760 ++++++++++ 13 files changed, 3737 insertions(+), 124 deletions(-) create mode 100644 kentikapi/internal/convert/convert.go create mode 100644 kentikapi/internal/convert/convert_test.go create mode 100644 kentikapi/internal/synthetics/test.go create mode 100644 kentikapi/synth_test_integration_test.go create mode 100644 kentikapi/synthetics/test.go diff --git a/README.md b/README.md index b78bc83..7c09a27 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,8 @@ Usage examples are placed in the [examples](./examples) directory. Note that exa ### Running examples +Note that examples are run against production Kentik API server, so Kentik production resources might be modified. + Run an example: ```bash @@ -33,7 +35,7 @@ export KTAPI_AUTH_EMAIL= export KTAPI_AUTH_TOKEN= # Run from a Go module, e.g. the root of this repository -# Adjust -run parameter to filter example names +# Adjust -run parameter to filter example names, e.g. "Users", "Synthetics", "SyntheticsTestsAPI" go test -tags examples -count 1 -parallel 1 -v -run Users github.com/kentik/community_sdk_golang/examples ``` diff --git a/examples/cloud_export_example_test.go b/examples/cloud_export_example_test.go index b0325c2..4ca09fe 100644 --- a/examples/cloud_export_example_test.go +++ b/examples/cloud_export_example_test.go @@ -54,7 +54,12 @@ func demonstrateCloudAPIWithAWSExport() error { fmt.Println("Got all cloud exports:") PrettyPrint(getAllResp.Exports) fmt.Println("Number of cloud exports:", len(getAllResp.Exports)) - fmt.Println("Invalid cloud exports count:", getAllResp.InvalidExportsCount) + if getAllResp.InvalidExportsCount > 0 { + fmt.Printf( + "Kentik API returned %v invalid cloud exports. Please, contact Kentik support.\n", + getAllResp.InvalidExportsCount, + ) + } fmt.Println("### Creating AWS cloud export") ce := cloud.NewAWSExport(cloud.AWSExportRequiredFields{ diff --git a/examples/synthetics_example_test.go b/examples/synthetics_example_test.go index 8b32e0d..7fc2fa5 100644 --- a/examples/synthetics_example_test.go +++ b/examples/synthetics_example_test.go @@ -7,19 +7,25 @@ import ( "context" "errors" "fmt" + "log" + "net" + "net/http" + "net/url" "strings" "testing" "time" syntheticspb "github.com/kentik/api-schema-public/gen/go/kentik/synthetics/v202202" "github.com/kentik/community_sdk_golang/kentikapi" + "github.com/kentik/community_sdk_golang/kentikapi/models" + "github.com/kentik/community_sdk_golang/kentikapi/synthetics" "github.com/stretchr/testify/assert" "google.golang.org/protobuf/types/known/timestamppb" ) func TestDemonstrateSyntheticsAgentsAPI(t *testing.T) { t.Parallel() - err := demonstrateSyntheticsAgentAPI() + err := demonstrateSyntheticsAgentsAPI() assert.NoError(t, err) } @@ -29,16 +35,26 @@ func TestDemonstrateSyntheticsTestsAPI(t *testing.T) { assert.NoError(t, err) } +// TestDemonstrateSyntheticsTestsAPI_CreateMinimalTests demonstrates creating synthetics tests with only +// required fields set. +func TestDemonstrateSyntheticsTestsAPI_CreateMinimalTests(t *testing.T) { + t.Parallel() + err := createMinimalTests() + assert.NoError(t, err) +} + func TestDemonstrateSyntheticsDataServiceAPI(t *testing.T) { t.Parallel() err := demonstrateSyntheticsDataServiceAPI() assert.NoError(t, err) } -// demonstrateSyntheticsAgentAPI demonstrates available methods of Synthetics Agent API. -// Note that there is no create method in the API. -// Delete method exists but is omitted here, because of lack of create method. -func demonstrateSyntheticsAgentAPI() error { +// demonstrateSyntheticsAgentsAPI demonstrates available methods of Synthetics Agent API. +// Delete method exists but is omitted here, because of lack of create method in the API. +// If you have no private agent at your environment, you can replace pickPrivateAgentID function call with e.g. +// pickIPV4RustAgentID. However, it is not possible to modify (update/activate/deactivate) global agent, +// so those pieces of code need to be commented out in such case. +func demonstrateSyntheticsAgentsAPI() error { ctx := context.Background() client, err := NewClient() if err != nil { @@ -53,11 +69,17 @@ func demonstrateSyntheticsAgentAPI() error { fmt.Printf("Got all agents: %v\n", getAllResp) fmt.Println("Number of agents:", len(getAllResp.Agents)) - fmt.Println("Number of invalid agents:", getAllResp.InvalidAgentsCount) + if getAllResp.InvalidAgentsCount > 0 { + fmt.Printf( + "Kentik API returned %v invalid agents. Please, contact Kentik support.\n", + getAllResp.InvalidAgentsCount, + ) + } - agentID, err := pickPrivateAgentID(ctx) + // Pick a private agent, so it is possible to modify it + agentID, err := pickPrivateAgentID(getAllResp.Agents) if err != nil { - return fmt.Errorf("pick agent ID: %w", err) + return fmt.Errorf("pick private agent ID: %w", err) } fmt.Println("### Getting synthetics agent with ID", agentID) @@ -114,27 +136,6 @@ func demonstrateSyntheticsAgentAPI() error { return nil } -func pickPrivateAgentID(ctx context.Context) (string, error) { - client, err := NewClient() - if err != nil { - return "", err - } - - getAllResp, err := client.SyntheticsAdmin.ListAgents(ctx, &syntheticspb.ListAgentsRequest{}) - if err != nil { - return "", fmt.Errorf("client.SyntheticsAdmin.ListAgents: %w", err) - } - - if getAllResp.GetAgents() != nil { - for _, agent := range getAllResp.GetAgents() { - if agent.GetType() == "private" { - return agent.GetId(), nil - } - } - } - return "", fmt.Errorf("no private agent found: %w", err) -} - func demonstrateSyntheticsTestsAPI() error { ctx := context.Background() client, err := NewClient() @@ -143,125 +144,495 @@ func demonstrateSyntheticsTestsAPI() error { } fmt.Println("### Getting all synthetic tests") - getAllResp, err := client.SyntheticsAdmin.ListTests(ctx, &syntheticspb.ListTestsRequest{}) + getAllResp, err := client.Synthetics.GetAllTests(ctx) if err != nil { - return fmt.Errorf("client.SyntheticsAdmin.ListTests: %w", err) + return fmt.Errorf("client.Synthetics.GetAllTests: %w", err) } fmt.Println("Got all tests:", getAllResp) - fmt.Println("Number of tests:", len(getAllResp.GetTests())) - fmt.Println("Number of invalid tests:", getAllResp.InvalidCount) - fmt.Println() + fmt.Println("Number of tests:", len(getAllResp.Tests)) + if getAllResp.InvalidTestsCount > 0 { + fmt.Printf( + "Kentik API returned %v invalid tests. Please, contact Kentik support.\n", + getAllResp.InvalidTestsCount, + ) + } fmt.Println("### Creating hostname synthetic test") - createResp, err := client.SyntheticsAdmin.CreateTest(ctx, &syntheticspb.CreateTestRequest{Test: makeHostnameTest()}) + test, err := newHostnameTest(ctx, client) if err != nil { - return fmt.Errorf("client.SyntheticsAdmin.CreateTest: %w", err) + return fmt.Errorf("new hostname test: %w", err) } - fmt.Println("Created test:", createResp.String()) - fmt.Println() - - fmt.Println("### Setting synthetic test status to paused") - _, err = client.SyntheticsAdmin.SetTestStatus(ctx, &syntheticspb.SetTestStatusRequest{ - Id: createResp.Test.Id, - Status: syntheticspb.TestStatus_TEST_STATUS_PAUSED, - }) + test, err = client.Synthetics.CreateTest(ctx, test) if err != nil { - return fmt.Errorf("client.SyntheticsAdmin.SetTestStatus: %w", err) + return fmt.Errorf("client.SyntheticsAdmin.CreateTest: %w", err) } - fmt.Println("Set synthetic test status successfully") - fmt.Println() + + fmt.Println("Created test:") + PrettyPrint(test) fmt.Println("### Getting created synthetic test") - getReqPayLoad := &syntheticspb.GetTestRequest{Id: createResp.Test.Id} - getResp, err := client.SyntheticsAdmin.GetTest(ctx, getReqPayLoad) + test, err = client.Synthetics.GetTest(ctx, test.ID) if err != nil { - return fmt.Errorf("client.SyntheticsAdmin.GetTest: %w", err) + return fmt.Errorf("client.Synthetics.GetTest: %w", err) } - fmt.Println("Got test:", getResp) - fmt.Println() + + fmt.Println("Got test:") + PrettyPrint(test) + fmt.Println("Test's target hostname:", test.Settings.GetHostnameDefinition().Target) fmt.Println("### Updating synthetic test") - test := getResp.Test - test.Name = "go-sdk-updated-hostname-test" + test.Name = "go-sdk-example-updated-hostname-test" + test.Settings.Period = time.Second + test.Settings.Ping.Timeout = time.Millisecond + test.Settings.Traceroute.Limit = 1 - updateResp, err := client.SyntheticsAdmin.UpdateTest(ctx, &syntheticspb.UpdateTestRequest{Test: test}) + test, err = client.Synthetics.UpdateTest(ctx, test) if err != nil { return fmt.Errorf("client.SyntheticsAdmin.UpdateTest: %w", err) } - fmt.Println("Updated test:", updateResp) - fmt.Println() + + fmt.Println("Updated test:") + PrettyPrint(test) + + fmt.Println("### Setting synthetic test status to paused") + err = client.Synthetics.SetTestStatus(ctx, test.ID, synthetics.TestStatusPaused) + if err != nil { + return fmt.Errorf("client.Synthetics.SetTestStatus: %w", err) + } + fmt.Println("Set synthetic test status successfully") fmt.Println("### Deleting synthetic test") - _, err = client.SyntheticsAdmin.DeleteTest(ctx, &syntheticspb.DeleteTestRequest{Id: test.Id}) + err = client.Synthetics.DeleteTest(ctx, test.ID) if err != nil { - return fmt.Errorf("client.SyntheticsAdmin.DeleteTest: %w", err) + return fmt.Errorf("client.Synthetics.DeleteTest: %w", err) } fmt.Println("Deleted synthetic test successfully") - fmt.Println() return nil } -func makeHostnameTest() *syntheticspb.Test { - return &syntheticspb.Test{ - Name: "go-sdk-example-hostname-test", - Type: "hostname", - Status: syntheticspb.TestStatus_TEST_STATUS_ACTIVE, - Settings: &syntheticspb.TestSettings{ - Definition: &syntheticspb.TestSettings_Hostname{ - Hostname: &syntheticspb.HostnameTest{Target: "www.example.com"}, +func newHostnameTest(ctx context.Context, client *kentikapi.Client) (*synthetics.Test, error) { + getAllResp, err := client.Synthetics.GetAllAgents(ctx) + if err != nil { + return nil, fmt.Errorf("client.Synthetics.GetAllAgents: %w", err) + } + + agentID, err := pickIPV4RustAgentID(getAllResp.Agents) + if err != nil { + return nil, fmt.Errorf("pick agent ID for hostname test: %w", err) + } + + test := synthetics.NewHostnameTest(synthetics.HostnameTestRequiredFields{ + BasePingTraceTestRequiredFields: synthetics.BasePingTraceTestRequiredFields{ + BaseTestRequiredFields: synthetics.BaseTestRequiredFields{ + Name: "go-sdk-example-hostname-test", + AgentIDs: []string{agentID}, }, - AgentIds: []string{"890"}, - Tasks: []string{ - "ping", - "traceroute", + Ping: synthetics.PingSettingsRequiredFields{ + Timeout: 10 * time.Second, + Count: 10, + Protocol: synthetics.PingProtocolTCP, + Port: 65535, + }, + Traceroute: synthetics.TracerouteSettingsRequiredFields{ + Timeout: 59999 * time.Millisecond, + Count: 5, + Delay: 100 * time.Millisecond, + Protocol: synthetics.TracerouteProtocolUDP, + Limit: 255, }, - HealthSettings: &syntheticspb.HealthSettings{ - LatencyCritical: 1, - LatencyWarning: 2, - PacketLossCritical: 3, - PacketLossWarning: 4, - JitterCritical: 5, - JitterWarning: 6, - HttpLatencyCritical: 7, - HttpLatencyWarning: 8, - HttpValidCodes: []uint32{200, 201}, - DnsValidCodes: []uint32{1, 2, 3}, - LatencyCriticalStddev: 9, - LatencyWarningStddev: 10, - JitterCriticalStddev: 11, - JitterWarningStddev: 12, - HttpLatencyCriticalStddev: 13, - HttpLatencyWarningStddev: 14, - UnhealthySubtestThreshold: 15, - Activation: &syntheticspb.ActivationSettings{ - GracePeriod: "2", - TimeUnit: "m", - TimeWindow: "5", - Times: "3", - }, + }, + Definition: synthetics.TestDefinitionHostnameRequiredFields{ + Target: "www.example.com", + }, + }) + + test.Settings.Period = 15 * time.Minute + test.Settings.Family = synthetics.IPFamilyV4 + test.Settings.NotificationChannels = []string{} // must contain IDs of existing notification channels + test.Settings.Health = synthetics.HealthSettings{ + LatencyCritical: 50 * time.Millisecond, + LatencyWarning: 20 * time.Millisecond, + LatencyCriticalStdDev: 100 * time.Millisecond, + LatencyWarningStdDev: 100 * time.Millisecond, + JitterCritical: 50 * time.Millisecond, + JitterWarning: 20 * time.Millisecond, + JitterCriticalStdDev: 100 * time.Millisecond, + JitterWarningStdDev: 100 * time.Millisecond, + PacketLossCritical: 100, + PacketLossWarning: 100, + HTTPLatencyCritical: 50 * time.Millisecond, + HTTPLatencyWarning: 20 * time.Millisecond, + HTTPLatencyCriticalStdDev: 100 * time.Millisecond, + HTTPLatencyWarningStdDev: 100 * time.Millisecond, + HTTPValidCodes: []uint32{http.StatusOK, http.StatusCreated}, + DNSValidCodes: []uint32{1, 2, 3}, + UnhealthySubtestThreshold: 2, + AlarmActivation: &synthetics.AlarmActivationSettings{ + TimeWindow: 75 * time.Minute, + Times: 4, + GracePeriod: 3, + }, + } + test.Settings.Ping.Delay = 100 * time.Millisecond + test.Settings.Ping.Port = 65535 + test.Settings.Traceroute.Port = 1 + + return test, nil +} + +func createMinimalTests() error { + ctx := context.Background() + client, err := NewClient() + if err != nil { + return err + } + + getAllResp, err := client.Synthetics.GetAllAgents(ctx) + if err != nil { + return fmt.Errorf("client.Synthetics.GetAllAgents: %w", err) + } + + rustAgentIDs := filterIPV4RustAgentIDs(getAllResp.Agents) + if len(rustAgentIDs) < 2 { + return fmt.Errorf("insufficient number of IPv4 Rust agents found: %v", len(rustAgentIDs)) + } + + nodeAgentID, err := pickIPV4NodeAgentID(getAllResp.Agents) + if err != nil { + return err + } + + for _, t := range []*synthetics.Test{ + newMinimalIPTest(rustAgentIDs[0:1]), + newMinimalNetworkGridTest(rustAgentIDs[0:1]), + newMinimalHostnameTest(rustAgentIDs[0:1]), + newMinimalAgentTest(rustAgentIDs[0:1]), + newMinimalNetworkMeshTest(rustAgentIDs[0:2]), // multiple agents required + newMinimalFlowTest(rustAgentIDs[0:1]), + newMinimalURLTest(rustAgentIDs[0:1]), + newMinimalPageLoadTest([]models.ID{nodeAgentID}), // agent with implementation type Node required + newMinimalDNSTest(rustAgentIDs[0:1]), + newMinimalDNSGridTest(rustAgentIDs[0:1]), + } { + err = createAndDeleteTest(ctx, client, t) + if err != nil { + return err + } + } + + return nil +} + +func createAndDeleteTest(ctx context.Context, client *kentikapi.Client, test *synthetics.Test) error { + fmt.Println("### Creating synthetic test", test.Name) + tName := test.Name // test object is nil on error + test, err := client.Synthetics.CreateTest(ctx, test) + if err != nil { + return fmt.Errorf("create test %q: %w", tName, err) + } + + fmt.Println("Created synthetic test:") + PrettyPrint(test) + + fmt.Println("### Deleting synthetic test", test.Name) + err = client.Synthetics.DeleteTest(ctx, test.ID) + if err != nil { + return fmt.Errorf("delete test %q: %w", test.Name, err) + } + fmt.Printf("Deleted synthetic test %q successfully\n", test.Name) + return nil +} + +func newMinimalIPTest(agentIDs []models.ID) *synthetics.Test { + return synthetics.NewIPTest(synthetics.IPTestRequiredFields{ + BasePingTraceTestRequiredFields: synthetics.BasePingTraceTestRequiredFields{ + BaseTestRequiredFields: synthetics.BaseTestRequiredFields{ + Name: "go-sdk-example-minimal-ip-test", + AgentIDs: agentIDs, }, - Ping: &syntheticspb.TestPingSettings{ + Ping: synthetics.PingSettingsRequiredFields{ + Timeout: 10 * time.Second, Count: 10, - Protocol: "icmp", - Port: 0, - Timeout: 10000, - Delay: 100, + Protocol: synthetics.PingProtocolTCP, + Port: 65535, }, - Trace: &syntheticspb.TestTraceSettings{ + Traceroute: synthetics.TracerouteSettingsRequiredFields{ + Timeout: 59999 * time.Millisecond, Count: 5, - Protocol: "tcp", - Port: 443, - Timeout: 59999, + Delay: 100 * time.Millisecond, + Protocol: synthetics.TracerouteProtocolUDP, Limit: 255, - Delay: 100, }, - Period: 60, - Family: syntheticspb.IPFamily_IP_FAMILY_DUAL, }, + Definition: synthetics.TestDefinitionIPRequiredFields{ + Targets: []net.IP{net.ParseIP("192.0.2.213"), net.ParseIP("192.0.2.214")}, + }, + }) +} + +func newMinimalNetworkGridTest(agentIDs []models.ID) *synthetics.Test { + return synthetics.NewNetworkGridTest(synthetics.NetworkGridTestRequiredFields{ + BasePingTraceTestRequiredFields: synthetics.BasePingTraceTestRequiredFields{ + BaseTestRequiredFields: synthetics.BaseTestRequiredFields{ + Name: "go-sdk-example-minimal-network-grid-test", + AgentIDs: agentIDs, + }, + Ping: synthetics.PingSettingsRequiredFields{ + Timeout: 10 * time.Second, + Count: 10, + Protocol: synthetics.PingProtocolTCP, + Port: 65535, + }, + Traceroute: synthetics.TracerouteSettingsRequiredFields{ + Timeout: 59999 * time.Millisecond, + Count: 5, + Delay: 100 * time.Millisecond, + Protocol: synthetics.TracerouteProtocolUDP, + Limit: 255, + }, + }, + Definition: synthetics.TestDefinitionNetworkGridRequiredFields{ + Targets: []net.IP{net.ParseIP("192.0.2.213"), net.ParseIP("192.0.2.214")}, + }, + }) +} + +func newMinimalHostnameTest(agentIDs []models.ID) *synthetics.Test { + return synthetics.NewHostnameTest(synthetics.HostnameTestRequiredFields{ + BasePingTraceTestRequiredFields: synthetics.BasePingTraceTestRequiredFields{ + BaseTestRequiredFields: synthetics.BaseTestRequiredFields{ + Name: "go-sdk-example-minimal-hostname-test", + AgentIDs: agentIDs, + }, + Ping: synthetics.PingSettingsRequiredFields{ + Timeout: 10 * time.Second, + Count: 10, + Protocol: synthetics.PingProtocolTCP, + Port: 65535, + }, + Traceroute: synthetics.TracerouteSettingsRequiredFields{ + Timeout: 59999 * time.Millisecond, + Count: 5, + Delay: 100 * time.Millisecond, + Protocol: synthetics.TracerouteProtocolUDP, + Limit: 255, + }, + }, + Definition: synthetics.TestDefinitionHostnameRequiredFields{ + Target: "www.example.com", + }, + }) +} + +func newMinimalAgentTest(agentIDs []models.ID) *synthetics.Test { + return synthetics.NewAgentTest(synthetics.AgentTestRequiredFields{ + BasePingTraceTestRequiredFields: synthetics.BasePingTraceTestRequiredFields{ + BaseTestRequiredFields: synthetics.BaseTestRequiredFields{ + Name: "go-sdk-example-minimal-agent-test", + AgentIDs: agentIDs, + }, + Ping: synthetics.PingSettingsRequiredFields{ + Timeout: 10 * time.Second, + Count: 10, + Protocol: synthetics.PingProtocolICMP, + }, + Traceroute: synthetics.TracerouteSettingsRequiredFields{ + Timeout: 59999 * time.Millisecond, + Count: 5, + Delay: 100 * time.Millisecond, + Protocol: synthetics.TracerouteProtocolUDP, + Limit: 255, + }, + }, + Definition: synthetics.TestDefinitionAgentRequiredFields{ + Target: agentIDs[0], + }, + }) +} + +func newMinimalNetworkMeshTest(agentIDs []models.ID) *synthetics.Test { + return synthetics.NewNetworkMeshTest(synthetics.NetworkMeshTestRequiredFields{ + BasePingTraceTestRequiredFields: synthetics.BasePingTraceTestRequiredFields{ + BaseTestRequiredFields: synthetics.BaseTestRequiredFields{ + Name: "go-sdk-example-minimal-network-mesh-test", + AgentIDs: agentIDs, + }, + Ping: synthetics.PingSettingsRequiredFields{ + Timeout: 10 * time.Second, + Count: 10, + Protocol: synthetics.PingProtocolICMP, + }, + Traceroute: synthetics.TracerouteSettingsRequiredFields{ + Timeout: 59999 * time.Millisecond, + Count: 5, + Delay: 100 * time.Millisecond, + Protocol: synthetics.TracerouteProtocolUDP, + Limit: 255, + }, + }, + }) +} + +func newMinimalFlowTest(agentIDs []models.ID) *synthetics.Test { + return synthetics.NewFlowTest(synthetics.FlowTestRequiredFields{ + BasePingTraceTestRequiredFields: synthetics.BasePingTraceTestRequiredFields{ + BaseTestRequiredFields: synthetics.BaseTestRequiredFields{ + Name: "go-sdk-example-minimal-flow-test", + AgentIDs: agentIDs, + }, + Ping: synthetics.PingSettingsRequiredFields{ + Timeout: 10 * time.Second, + Count: 10, + Protocol: synthetics.PingProtocolICMP, + }, + Traceroute: synthetics.TracerouteSettingsRequiredFields{ + Timeout: 59999 * time.Millisecond, + Count: 5, + Delay: 100 * time.Millisecond, + Protocol: synthetics.TracerouteProtocolUDP, + Limit: 255, + }, + }, + Definition: synthetics.TestDefinitionFlowRequiredFields{ + Type: synthetics.FlowTestTypeCity, + Target: "Warsaw", + Direction: synthetics.DirectionSrc, + InetDirection: synthetics.DirectionDst, + }, + }) +} + +func newMinimalURLTest(agentIDs []models.ID) *synthetics.Test { + return synthetics.NewURLTest(synthetics.URLTestRequiredFields{ + BaseTestRequiredFields: synthetics.BaseTestRequiredFields{ + Name: "go-sdk-example-minimal-url-test", + AgentIDs: agentIDs, + }, + Definition: synthetics.TestDefinitionURLRequiredFields{ + Target: url.URL{ + Scheme: "https", + Host: "www.example.com:443", + RawQuery: "dummy=query", + }, + Timeout: time.Minute, + }, + }) +} + +func newMinimalPageLoadTest(agentIDs []models.ID) *synthetics.Test { + return synthetics.NewPageLoadTest(synthetics.PageLoadTestRequiredFields{ + BaseTestRequiredFields: synthetics.BaseTestRequiredFields{ + Name: "go-sdk-example-minimal-page-load-test", + AgentIDs: agentIDs, + }, + Definition: synthetics.TestDefinitionPageLoadRequiredFields{ + Target: url.URL{ + Scheme: "https", + Host: "www.example.com:443", + RawQuery: "dummy=query", + }, + Timeout: time.Minute, + }, + }) +} + +func newMinimalDNSTest(agentIDs []models.ID) *synthetics.Test { + return synthetics.NewDNSTest(synthetics.DNSTestRequiredFields{ + BaseTestRequiredFields: synthetics.BaseTestRequiredFields{ + Name: "go-sdk-example-minimal-page-load-test", + AgentIDs: agentIDs, + }, + Definition: synthetics.TestDefinitionDNSRequiredFields{ + Target: "www.example.com", + Timeout: time.Minute, + RecordType: synthetics.DNSRecordAAAA, + Servers: []net.IP{net.ParseIP("192.0.2.213"), net.ParseIP("2001:db8:dead:beef:dead:beef:dead:beef")}, + Port: 53, + }, + }) +} + +func newMinimalDNSGridTest(agentIDs []models.ID) *synthetics.Test { + return synthetics.NewDNSGridTest(synthetics.DNSGridTestRequiredFields{ + BaseTestRequiredFields: synthetics.BaseTestRequiredFields{ + Name: "go-sdk-example-minimal-page-load-test", + AgentIDs: agentIDs, + }, + Definition: synthetics.TestDefinitionDNSGridRequiredFields{ + Target: "www.example.com", + Timeout: time.Minute, + RecordType: synthetics.DNSRecordAAAA, + Servers: []net.IP{net.ParseIP("192.0.2.213"), net.ParseIP("2001:db8:dead:beef:dead:beef:dead:beef")}, + Port: 53, + }, + }) +} + +func pickPrivateAgentID(agents []synthetics.Agent) (models.ID, error) { + var matchedIDs []models.ID + for _, a := range agents { + if a.Type == synthetics.AgentTypePrivate { + matchedIDs = append(matchedIDs, a.ID) + } + } + + if len(matchedIDs) == 0 { + return "", fmt.Errorf("no agent meeting criteria (AgentTypePrivate) found") + } + + agentID := matchedIDs[0] + log.Printf( + "Found %v agents meeting criteria (AgentTypePrivate), picked agent with ID %v\n", + len(matchedIDs), agentID, + ) + return agentID, nil +} + +func pickIPV4RustAgentID(agents []synthetics.Agent) (models.ID, error) { + matchedIDs := filterIPV4RustAgentIDs(agents) + if len(matchedIDs) == 0 { + return "", fmt.Errorf("no agent meeting criteria (IPFamilyV4, AgentImplementationTypeRust) found") } + + agentID := matchedIDs[0] + log.Printf( + "Found %v agents meeting criteria (IPFamilyV4, AgentImplementationTypeRust), picked agent with ID %v\n", + len(matchedIDs), agentID, + ) + return agentID, nil +} + +func filterIPV4RustAgentIDs(agents []synthetics.Agent) []models.ID { + var matchedIDs []models.ID + for _, a := range agents { + if a.IPFamily == synthetics.IPFamilyV4 && a.ImplementationType == synthetics.AgentImplementationTypeRust { + matchedIDs = append(matchedIDs, a.ID) + } + } + return matchedIDs +} + +func pickIPV4NodeAgentID(agents []synthetics.Agent) (models.ID, error) { + var matchedIDs []models.ID + for _, a := range agents { + if a.IPFamily == synthetics.IPFamilyV4 && a.ImplementationType == synthetics.AgentImplementationTypeNode { + matchedIDs = append(matchedIDs, a.ID) + } + } + + if len(matchedIDs) == 0 { + return "", fmt.Errorf("no agent meeting criteria (IPFamilyV4, AgentImplementationTypeNode) found") + } + + agentID := matchedIDs[0] + log.Printf( + "Found %v agents meeting criteria (IPFamilyV4, AgentImplementationTypeNode), picked agent with ID %v\n", + len(matchedIDs), agentID, + ) + return agentID, nil } func demonstrateSyntheticsDataServiceAPI() error { diff --git a/kentikapi/internal/cloud/cloud.go b/kentikapi/internal/cloud/cloud.go index 4bec011..453c64e 100644 --- a/kentikapi/internal/cloud/cloud.go +++ b/kentikapi/internal/cloud/cloud.go @@ -10,12 +10,12 @@ import ( "google.golang.org/grpc" ) -// API aggregates Cloud Exports API methods. +// API aggregates cloudexports API methods. type API struct { client cloudexportpb.CloudExportAdminServiceClient } -// NewAPI creates new cloud API. +// NewAPI creates new cloudexports API. func NewAPI(cc grpc.ClientConnInterface) *API { return &API{ client: cloudexportpb.NewCloudExportAdminServiceClient(cc), diff --git a/kentikapi/internal/cloud/export.go b/kentikapi/internal/cloud/export.go index 76f553b..fcf9500 100644 --- a/kentikapi/internal/cloud/export.go +++ b/kentikapi/internal/cloud/export.go @@ -88,7 +88,7 @@ func propertiesFromPayload(ce *cloudexportpb.CloudExport) (cloud.ExportPropertie case ibmProvider: return ibmPropertiesFromPayload(ce.GetIbm()) default: - return nil, fmt.Errorf("invalid cloud provider in response payload: %v", ce.CloudProvider) + return nil, fmt.Errorf("unsupported cloud provider in response payload: %v", ce.CloudProvider) } } @@ -211,7 +211,7 @@ func cePayloadWithProperties(payload *cloudexportpb.CloudExport, ce *cloud.Expor case ibmProvider: payload.Properties = ibmPropertiesToPayload(ce) default: - return nil, fmt.Errorf("invalid cloud provider: %v", ce.Provider) + return nil, fmt.Errorf("unsupported cloud provider: %v", ce.Provider) } return payload, nil } diff --git a/kentikapi/internal/convert/convert.go b/kentikapi/internal/convert/convert.go new file mode 100644 index 0000000..52818e4 --- /dev/null +++ b/kentikapi/internal/convert/convert.go @@ -0,0 +1,53 @@ +package convert + +import ( + "fmt" + "net" + "time" + + "github.com/AlekSi/pointer" + "google.golang.org/protobuf/types/known/timestamppb" +) + +func MillisecondsF32ToDuration(ms float32) time.Duration { + // scale to nanoseconds before conversion to duration to minimise conversion loss + const nanosPerMilli = 1e6 + return time.Duration(nanosPerMilli*ms) * time.Nanosecond +} + +func StringsToIPs(ips []string) ([]net.IP, error) { + result := make([]net.IP, 0, len(ips)) + for _, ipStr := range ips { + ip := net.ParseIP(ipStr) + if ip == nil { + return nil, fmt.Errorf("invalid IP: %v", ipStr) + } + + result = append(result, ip) + } + return result, nil +} + +func IPsToStrings(ips []net.IP) []string { + result := make([]string, 0, len(ips)) + for _, ip := range ips { + result = append(result, ip.String()) + } + return result +} + +func TimestampPtrToTime(ts *timestamppb.Timestamp) *time.Time { + if ts == nil { + return nil + } + + return pointer.ToTime(ts.AsTime()) +} + +func TimePtrToTimestamp(t *time.Time) *timestamppb.Timestamp { + if t == nil { + return nil + } + + return timestamppb.New(*t) +} diff --git a/kentikapi/internal/convert/convert_test.go b/kentikapi/internal/convert/convert_test.go new file mode 100644 index 0000000..909fff1 --- /dev/null +++ b/kentikapi/internal/convert/convert_test.go @@ -0,0 +1,34 @@ +package convert_test + +import ( + "fmt" + "testing" + "time" + + "github.com/kentik/community_sdk_golang/kentikapi/internal/convert" + "github.com/stretchr/testify/assert" +) + +func TestMillisecondsToDuration(t *testing.T) { + tests := []struct { + ms float32 + expected time.Duration + }{ + { + ms: 1000, + expected: time.Second, + }, { + ms: 1.1, + expected: 1100 * time.Microsecond, + }, { + ms: 0.5, + expected: 500 * time.Microsecond, + }, + } + for _, tt := range tests { + t.Run(fmt.Sprint(tt.ms), func(t *testing.T) { + result := convert.MillisecondsF32ToDuration(tt.ms) + assert.Equal(t, tt.expected, result) + }) + } +} diff --git a/kentikapi/internal/synthetics/synthetics.go b/kentikapi/internal/synthetics/synthetics.go index caf0352..1ded713 100644 --- a/kentikapi/internal/synthetics/synthetics.go +++ b/kentikapi/internal/synthetics/synthetics.go @@ -2,8 +2,10 @@ package synthetics import ( "context" + "errors" "fmt" "log" + "time" syntheticspb "github.com/kentik/api-schema-public/gen/go/kentik/synthetics/v202202" kentikerrors "github.com/kentik/community_sdk_golang/kentikapi/internal/errors" @@ -12,7 +14,7 @@ import ( "google.golang.org/grpc" ) -// API aggregates Synthetics API methods. +// API aggregates synthetics API methods. type API struct { client syntheticspb.SyntheticsAdminServiceClient } @@ -129,3 +131,128 @@ func (a *API) DeactivateAgent(ctx context.Context, id models.ID) (*synthetics.Ag return agent, nil } + +// GetAllTests lists synthetics tests. +func (a *API) GetAllTests(ctx context.Context) (*synthetics.GetAllTestsResponse, error) { + respPayload, err := a.client.ListTests(ctx, &syntheticspb.ListTestsRequest{}) + if err != nil { + return nil, kentikerrors.StatusErrorFromGRPC(err) + } + + resp, err := (*listTestsResponse)(respPayload).ToModel() + if err != nil { + return nil, kentikerrors.New(kentikerrors.InvalidResponse, err.Error()) + } + + return resp, nil +} + +// GetTest retrieves synthetics test with given ID. +func (a *API) GetTest(ctx context.Context, id models.ID) (*synthetics.Test, error) { + respPayload, err := a.client.GetTest(ctx, &syntheticspb.GetTestRequest{Id: id}) + if err != nil { + return nil, kentikerrors.StatusErrorFromGRPC(err) + } + + resp, err := testFromPayload(respPayload.GetTest()) + if err != nil { + return nil, kentikerrors.New(kentikerrors.InvalidResponse, err.Error()) + } + + return resp, nil +} + +// CreateTest creates the synthetics test. +func (a *API) CreateTest(ctx context.Context, test *synthetics.Test) (*synthetics.Test, error) { + test, err := testWithDefaultFields(test) + if err != nil { + return nil, kentikerrors.New(kentikerrors.InvalidRequest, err.Error()) + } + + // TODO(dfurman): validate create request + + reqPayload, err := testToPayload(test) + if err != nil { + return nil, kentikerrors.New(kentikerrors.InvalidRequest, err.Error()) + } + + respPayload, err := a.client.CreateTest(ctx, &syntheticspb.CreateTestRequest{Test: reqPayload}) + if err != nil { + return nil, kentikerrors.StatusErrorFromGRPC(err) + } + + resp, err := testFromPayload(respPayload.GetTest()) + if err != nil { + return nil, kentikerrors.New(kentikerrors.InvalidResponse, err.Error()) + } + + return resp, nil +} + +func testWithDefaultFields(t *synthetics.Test) (*synthetics.Test, error) { + if t == nil { + return nil, errors.New("test object is nil") + } + + if t.Status == "" { + t.Status = synthetics.TestStatusActive // field required by the server (but ignored on create) + } + + if t.Settings.Period == 0 { + t.Settings.Period = time.Minute // field required by the server + } + + if t.Settings.Family == "" { + t.Settings.Family = synthetics.IPFamilyDual // field required by the server + } + + if t.Settings.Health.UnhealthySubtestThreshold == 0 { + t.Settings.Health.UnhealthySubtestThreshold = 1 // field required by the server + } + + if t.Settings.Traceroute != nil && t.Settings.Traceroute.Protocol != synthetics.TracerouteProtocolICMP { + t.Settings.Traceroute.Port = 33434 + } + + return t, nil +} + +// UpdateTest updates the synthetics test. +func (a *API) UpdateTest(ctx context.Context, test *synthetics.Test) (*synthetics.Test, error) { + // TODO(dfurman): validate update request + + reqPayload, err := testToPayload(test) + if err != nil { + return nil, kentikerrors.New(kentikerrors.InvalidRequest, err.Error()) + } + + respPayload, err := a.client.UpdateTest(ctx, &syntheticspb.UpdateTestRequest{Test: reqPayload}) + if err != nil { + return nil, kentikerrors.StatusErrorFromGRPC(err) + } + + resp, err := testFromPayload(respPayload.GetTest()) + if err != nil { + return nil, kentikerrors.New(kentikerrors.InvalidResponse, err.Error()) + } + + return resp, nil +} + +// DeleteTest removes synthetics test with given ID. +func (a *API) DeleteTest(ctx context.Context, id models.ID) error { + _, err := a.client.DeleteTest(ctx, &syntheticspb.DeleteTestRequest{Id: id}) + return kentikerrors.StatusErrorFromGRPC(err) +} + +// SetTestStatus modifies status of the synthetics test with given ID. +func (a *API) SetTestStatus(ctx context.Context, id models.ID, ts synthetics.TestStatus) error { + _, err := a.client.SetTestStatus(ctx, &syntheticspb.SetTestStatusRequest{ + Id: id, + Status: syntheticspb.TestStatus(syntheticspb.TestStatus_value[string(ts)]), + }) + return kentikerrors.StatusErrorFromGRPC(err) +} + +// TODO(dfurman): client.Synthetics.GetTestResults() +// TODO(dfurman): client.Synthetics.GetTraceResults() diff --git a/kentikapi/internal/synthetics/test.go b/kentikapi/internal/synthetics/test.go new file mode 100644 index 0000000..b57d58e --- /dev/null +++ b/kentikapi/internal/synthetics/test.go @@ -0,0 +1,664 @@ +package synthetics + +import ( + "errors" + "fmt" + "net/url" + "strconv" + "time" + + syntheticspb "github.com/kentik/api-schema-public/gen/go/kentik/synthetics/v202202" + "github.com/kentik/community_sdk_golang/kentikapi/internal/convert" + "github.com/kentik/community_sdk_golang/kentikapi/synthetics" +) + +// Test types hidden from SDK users (not included in public enum). +const ( + testTypeBGPMonitor = "bgp_monitor" + testTypeTransaction = "transaction" +) + +type listTestsResponse syntheticspb.ListTestsResponse + +func (r *listTestsResponse) ToModel() (*synthetics.GetAllTestsResponse, error) { + if r == nil { + return nil, errors.New("response payload is nil") + } + + tests, err := testsFromPayload(r.Tests) + if err != nil { + return nil, err + } + + return &synthetics.GetAllTestsResponse{ + Tests: tests, + InvalidTestsCount: r.InvalidCount, + }, nil +} + +func testsFromPayload(tests []*syntheticspb.Test) ([]synthetics.Test, error) { + var result []synthetics.Test + for i, t := range tests { + if t.Type == testTypeBGPMonitor || t.Type == testTypeTransaction { + // Silently ignore BGP monitor tests, as they are going to be handled in separate BGP monitoring API + // Silently ignore transaction tests, as they cannot yet provide actual configuration or results + continue + } + + test, err := testFromPayload(t) + if err != nil { + return nil, fmt.Errorf("test with index %v: %w", i, err) + } + result = append(result, *test) + } + return result, nil +} + +// testFromPayload converts synthetics test payload to model. +func testFromPayload(t *syntheticspb.Test) (*synthetics.Test, error) { + if t == nil { + return nil, fmt.Errorf("response payload is nil") + } + + if t.Id == "" { + return nil, fmt.Errorf("empty test ID in response payload") + } + + settings, err := testSettingsFromPayload(t) + if err != nil { + return nil, fmt.Errorf("convert response payload to test settings: %w", err) + } + + createdBy := userInfoPtrFromPayload(t.CreatedBy) + if createdBy == nil { + return nil, errors.New("createdBy field is nil in response payload") + } + + return &synthetics.Test{ + Name: t.Name, + Status: synthetics.TestStatus(t.Status.String()), + UpdateDate: convert.TimestampPtrToTime(t.Edate), + Settings: settings, + ID: t.Id, + Type: synthetics.TestType(t.Type), + CreateDate: t.Cdate.AsTime(), + CreatedBy: *createdBy, + LastUpdatedBy: userInfoPtrFromPayload(t.LastUpdatedBy), + }, nil +} + +func testSettingsFromPayload(t *syntheticspb.Test) (synthetics.TestSettings, error) { + if t.Settings == nil { + return synthetics.TestSettings{}, fmt.Errorf("empty test settings") + } + ts := t.Settings + + definition, err := testDefinitionFromPayload(t) + if err != nil { + return synthetics.TestSettings{}, err + } + + hs, err := healthSettingsFromPayload(ts.HealthSettings) + if err != nil { + return synthetics.TestSettings{}, err + } + + return synthetics.TestSettings{ + Definition: definition, + AgentIDs: ts.AgentIds, + Tasks: taskTypesFromPayload(ts.Tasks), + Health: hs, + Ping: pingSettingsFromPayload(ts.Ping), + Traceroute: tracerouteSettingsFromPayload(ts.Trace), + Period: time.Duration(ts.Period) * time.Second, + Family: synthetics.IPFamily(ts.Family.String()), + NotificationChannels: ts.NotificationChannels, + }, nil +} + +// nolint: gocyclo +func testDefinitionFromPayload(t *syntheticspb.Test) (synthetics.TestDefinition, error) { + switch synthetics.TestType(t.Type) { + case synthetics.TestTypeIP: + return ipTestDefinitionFromPayload(t.Settings) + case synthetics.TestTypeNetworkGrid: + return networkGridTestDefinitionFromPayload(t.Settings) + case synthetics.TestTypeHostname: + return hostnameTestDefinitionFromPayload(t.Settings) + case synthetics.TestTypeAgent: + return agentTestDefinitionFromPayload(t.Settings) + case synthetics.TestTypeNetworkMesh: + return networkMeshTestDefinitionFromPayload(t.Settings) + case synthetics.TestTypeFlow: + return flowTestDefinitionFromPayload(t.Settings) + case synthetics.TestTypeURL: + return urlTestDefinitionFromPayload(t.Settings) + case synthetics.TestTypePageLoad: + return pageLoadTestDefinitionFromPayload(t.Settings) + case synthetics.TestTypeDNS: + return dnsTestDefinitionFromPayload(t.Settings) + case synthetics.TestTypeDNSGrid: + return dnsGridTestDefinitionFromPayload(t.Settings) + default: + return nil, fmt.Errorf("unsupported test type: %v", t.Type) + } +} + +func ipTestDefinitionFromPayload(ts *syntheticspb.TestSettings) (synthetics.TestDefinition, error) { + d := ts.GetIp() + if d == nil { + return nil, errors.New("IP test definition is nil") + } + + targets, err := convert.StringsToIPs(d.Targets) + if err != nil { + return nil, fmt.Errorf("convert IP targets: %v", err) + } + + return &synthetics.TestDefinitionIP{ + Targets: targets, + }, nil +} + +func networkGridTestDefinitionFromPayload(ts *syntheticspb.TestSettings) (synthetics.TestDefinition, error) { + d := ts.GetNetworkGrid() + if d == nil { + return nil, errors.New("network grid test definition is nil") + } + + targets, err := convert.StringsToIPs(d.Targets) + if err != nil { + return nil, fmt.Errorf("convert network grid targets: %v", err) + } + + return &synthetics.TestDefinitionNetworkGrid{ + Targets: targets, + }, nil +} + +func hostnameTestDefinitionFromPayload(ts *syntheticspb.TestSettings) (synthetics.TestDefinition, error) { + d := ts.GetHostname() + if d == nil { + return nil, errors.New("hostname test definition is nil") + } + + return &synthetics.TestDefinitionHostname{ + Target: d.Target, + }, nil +} + +func agentTestDefinitionFromPayload(ts *syntheticspb.TestSettings) (synthetics.TestDefinition, error) { + d := ts.GetAgent() + if d == nil { + return nil, errors.New("agent test definition is nil") + } + + return &synthetics.TestDefinitionAgent{ + Target: d.Target, + UseLocalIP: d.UseLocalIp, + }, nil +} + +func networkMeshTestDefinitionFromPayload(ts *syntheticspb.TestSettings) (synthetics.TestDefinition, error) { + d := ts.GetNetworkMesh() + if d == nil { + return nil, errors.New("network mesh test definition is nil") + } + + return &synthetics.TestDefinitionNetworkMesh{ + UseLocalIP: d.UseLocalIp, + }, nil +} + +func flowTestDefinitionFromPayload(ts *syntheticspb.TestSettings) (synthetics.TestDefinition, error) { + d := ts.GetFlow() + if d == nil { + return nil, errors.New("flow test definition is nil") + } + + return &synthetics.TestDefinitionFlow{ + Type: synthetics.FlowTestType(d.Type), + Target: d.Target, + TargetRefreshInterval: time.Duration(d.TargetRefreshIntervalMillis) * time.Millisecond, + MaxIPTargets: d.MaxIpTargets, + MaxProviders: d.MaxProviders, + Direction: synthetics.Direction(d.Direction), + InetDirection: synthetics.Direction(d.InetDirection), + }, nil +} + +func urlTestDefinitionFromPayload(ts *syntheticspb.TestSettings) (synthetics.TestDefinition, error) { + d := ts.GetUrl() + if d == nil { + return nil, errors.New("URL test definition is nil") + } + + target, err := url.Parse(d.Target) + if err != nil { + return nil, fmt.Errorf("parse URL test definition target: %v", err) + } + + return &synthetics.TestDefinitionURL{ + Target: *target, + Timeout: time.Duration(d.Timeout) * time.Millisecond, + Method: d.Method, + Headers: d.Headers, + Body: d.Body, + IgnoreTLSErrors: d.IgnoreTlsErrors, + }, nil +} + +func pageLoadTestDefinitionFromPayload(ts *syntheticspb.TestSettings) (synthetics.TestDefinition, error) { + d := ts.GetPageLoad() + if d == nil { + return nil, errors.New("page load test definition is nil") + } + + target, err := url.Parse(d.Target) + if err != nil { + return nil, fmt.Errorf("parse page load test definition target: %v", err) + } + + return &synthetics.TestDefinitionPageLoad{ + Target: *target, + Timeout: time.Duration(d.Timeout) * time.Millisecond, + Headers: d.Headers, + CSSSelectors: d.CssSelectors, + IgnoreTLSErrors: d.IgnoreTlsErrors, + }, nil +} + +func dnsTestDefinitionFromPayload(ts *syntheticspb.TestSettings) (synthetics.TestDefinition, error) { + d := ts.GetDns() + if d == nil { + return nil, errors.New("DNS test definition is nil") + } + + servers, err := convert.StringsToIPs(d.Servers) + if err != nil { + return nil, fmt.Errorf("convert DNS servers: %v", err) + } + + return &synthetics.TestDefinitionDNS{ + Target: d.Target, + Timeout: time.Duration(d.Timeout) * time.Millisecond, + RecordType: synthetics.DNSRecord(d.RecordType.String()), + Servers: servers, + Port: d.Port, + }, nil +} + +func dnsGridTestDefinitionFromPayload(ts *syntheticspb.TestSettings) (synthetics.TestDefinition, error) { + d := ts.GetDnsGrid() + if d == nil { + return nil, errors.New("DNS grid test definition is nil") + } + servers, err := convert.StringsToIPs(d.Servers) + if err != nil { + return nil, fmt.Errorf("convert DNS grid servers: %v", err) + } + + return &synthetics.TestDefinitionDNSGrid{ + Target: d.Target, + Timeout: time.Duration(d.Timeout) * time.Millisecond, + RecordType: synthetics.DNSRecord(d.RecordType.String()), + Servers: servers, + Port: d.Port, + }, nil +} + +func taskTypesFromPayload(tasks []string) []synthetics.TaskType { + var result []synthetics.TaskType + for _, t := range tasks { + result = append(result, synthetics.TaskType(t)) + } + return result +} + +func healthSettingsFromPayload(hs *syntheticspb.HealthSettings) (synthetics.HealthSettings, error) { + if hs == nil { + return synthetics.HealthSettings{}, fmt.Errorf("empty health settings") + } + + alarmActivation, err := alarmActivationFromPayload(hs.Activation) + if err != nil { + return synthetics.HealthSettings{}, err + } + + return synthetics.HealthSettings{ + LatencyCritical: convert.MillisecondsF32ToDuration(hs.LatencyCritical), + LatencyWarning: convert.MillisecondsF32ToDuration(hs.LatencyWarning), + LatencyCriticalStdDev: convert.MillisecondsF32ToDuration(hs.LatencyCriticalStddev), + LatencyWarningStdDev: convert.MillisecondsF32ToDuration(hs.LatencyWarningStddev), + JitterCritical: convert.MillisecondsF32ToDuration(hs.JitterCritical), + JitterWarning: convert.MillisecondsF32ToDuration(hs.JitterWarning), + JitterCriticalStdDev: convert.MillisecondsF32ToDuration(hs.JitterCriticalStddev), + JitterWarningStdDev: convert.MillisecondsF32ToDuration(hs.JitterWarningStddev), + PacketLossCritical: hs.PacketLossCritical, + PacketLossWarning: hs.PacketLossWarning, + HTTPLatencyCritical: convert.MillisecondsF32ToDuration(hs.HttpLatencyCritical), + HTTPLatencyWarning: convert.MillisecondsF32ToDuration(hs.HttpLatencyWarning), + HTTPLatencyCriticalStdDev: convert.MillisecondsF32ToDuration(hs.HttpLatencyCriticalStddev), + HTTPLatencyWarningStdDev: convert.MillisecondsF32ToDuration(hs.HttpLatencyWarningStddev), + HTTPValidCodes: hs.HttpValidCodes, + DNSValidCodes: hs.DnsValidCodes, + UnhealthySubtestThreshold: hs.UnhealthySubtestThreshold, + AlarmActivation: alarmActivation, + }, nil +} + +func pingSettingsFromPayload(ps *syntheticspb.TestPingSettings) *synthetics.PingSettings { + if ps == nil { + return nil + } + + return &synthetics.PingSettings{ + Timeout: time.Duration(ps.Timeout) * time.Millisecond, + Count: ps.Count, + Delay: convert.MillisecondsF32ToDuration(ps.Delay), + Protocol: synthetics.PingProtocol(ps.Protocol), + Port: ps.Port, + } +} + +func tracerouteSettingsFromPayload(ts *syntheticspb.TestTraceSettings) *synthetics.TracerouteSettings { + if ts == nil { + return nil + } + + return &synthetics.TracerouteSettings{ + Timeout: time.Duration(ts.Timeout) * time.Millisecond, + Count: ts.Count, + Delay: convert.MillisecondsF32ToDuration(ts.Delay), + Protocol: synthetics.TracerouteProtocol(ts.Protocol), + Port: ts.Port, + Limit: ts.Limit, + } +} + +func alarmActivationFromPayload(as *syntheticspb.ActivationSettings) (*synthetics.AlarmActivationSettings, error) { + if as == nil { + return nil, nil + } + + timeWindow, err := time.ParseDuration(as.TimeWindow + as.TimeUnit) + if err != nil { + return nil, fmt.Errorf("parse alarm activation time window %q: %v", as.TimeWindow+as.TimeUnit, err) + } + + times, err := strconv.ParseUint(as.Times, 10, 0) + if err != nil { + return nil, fmt.Errorf("parse alarm activation times %q: %v", as.Times, err) + } + + gracePeriod, err := strconv.ParseUint(as.GracePeriod, 10, 0) + if err != nil { + return nil, fmt.Errorf("parse alarm activation grace period %q: %v", as.GracePeriod, err) + } + + return &synthetics.AlarmActivationSettings{ + TimeWindow: timeWindow, + Times: uint(times), + GracePeriod: uint(gracePeriod), + }, nil +} + +func userInfoPtrFromPayload(ui *syntheticspb.UserInfo) *synthetics.UserInfo { + if ui == nil { + return nil + } + return &synthetics.UserInfo{ + ID: ui.Id, + Email: ui.Email, + FullName: ui.FullName, + } +} + +// testToPayload converts synthetics test from model to payload. It sets only ID and read-write fields. +func testToPayload(t *synthetics.Test) (*syntheticspb.Test, error) { + if t == nil { + return nil, errors.New("test object is nil") + } + + ts, err := testSettingsToPayload(t.Settings, t.Type) + if err != nil { + return nil, err + } + + return &syntheticspb.Test{ + Id: t.ID, + Name: t.Name, + Type: string(t.Type), + Status: syntheticspb.TestStatus(syntheticspb.TestStatus_value[string(t.Status)]), + Settings: ts, + Cdate: nil, // read-only + Edate: convert.TimePtrToTimestamp(t.UpdateDate), + CreatedBy: nil, // read-only + LastUpdatedBy: nil, // read-only + }, nil +} + +func testSettingsToPayload(ts synthetics.TestSettings, testType synthetics.TestType) (*syntheticspb.TestSettings, error) { + tsPayload := &syntheticspb.TestSettings{ + AgentIds: ts.AgentIDs, + Tasks: taskTypesToPayload(ts.Tasks), + HealthSettings: healthSettingsToPayload(ts.Health), + Ping: pingSettingsToPayload(ts.Ping), + Trace: tracerouteSettingsToPayload(ts.Traceroute), + Period: uint32(ts.Period / time.Second), + Family: syntheticspb.IPFamily(syntheticspb.IPFamily_value[string(ts.Family)]), + NotificationChannels: ts.NotificationChannels, + } + + return testSettingsPayloadWithDefinition(tsPayload, ts, testType) +} + +func taskTypesToPayload(tasks []synthetics.TaskType) []string { + var result []string + for _, t := range tasks { + result = append(result, string(t)) + } + return result +} + +func healthSettingsToPayload(hs synthetics.HealthSettings) *syntheticspb.HealthSettings { + return &syntheticspb.HealthSettings{ + LatencyCritical: float32(hs.LatencyCritical / time.Millisecond), + LatencyWarning: float32(hs.LatencyWarning / time.Millisecond), + PacketLossCritical: hs.PacketLossCritical, + PacketLossWarning: hs.PacketLossWarning, + JitterCritical: float32(hs.JitterCritical / time.Millisecond), + JitterWarning: float32(hs.JitterWarning / time.Millisecond), + HttpLatencyCritical: float32(hs.HTTPLatencyCritical / time.Millisecond), + HttpLatencyWarning: float32(hs.HTTPLatencyWarning / time.Millisecond), + HttpValidCodes: hs.HTTPValidCodes, + DnsValidCodes: hs.DNSValidCodes, + LatencyCriticalStddev: float32(hs.LatencyCriticalStdDev / time.Millisecond), + LatencyWarningStddev: float32(hs.LatencyWarningStdDev / time.Millisecond), + JitterCriticalStddev: float32(hs.JitterCriticalStdDev / time.Millisecond), + JitterWarningStddev: float32(hs.JitterWarningStdDev / time.Millisecond), + HttpLatencyCriticalStddev: float32(hs.HTTPLatencyCriticalStdDev / time.Millisecond), + HttpLatencyWarningStddev: float32(hs.HTTPLatencyWarningStdDev / time.Millisecond), + UnhealthySubtestThreshold: hs.UnhealthySubtestThreshold, + Activation: alarmActivationToPayload(hs.AlarmActivation), + } +} + +func alarmActivationToPayload(as *synthetics.AlarmActivationSettings) *syntheticspb.ActivationSettings { + if as == nil { + return nil + } + + return &syntheticspb.ActivationSettings{ + GracePeriod: strconv.FormatUint(uint64(as.GracePeriod), 10), + TimeUnit: "m", + TimeWindow: strconv.FormatInt(int64(as.TimeWindow/time.Minute), 10), + Times: strconv.FormatUint(uint64(as.Times), 10), + } +} + +func pingSettingsToPayload(ps *synthetics.PingSettings) *syntheticspb.TestPingSettings { + if ps == nil { + return nil + } + + return &syntheticspb.TestPingSettings{ + Count: ps.Count, + Protocol: string(ps.Protocol), + Port: ps.Port, + Timeout: uint32(ps.Timeout / time.Millisecond), + Delay: float32(ps.Delay / time.Millisecond), + } +} + +func tracerouteSettingsToPayload(ts *synthetics.TracerouteSettings) *syntheticspb.TestTraceSettings { + if ts == nil { + return nil + } + + return &syntheticspb.TestTraceSettings{ + Count: ts.Count, + Protocol: string(ts.Protocol), + Port: ts.Port, + Timeout: uint32(ts.Timeout / time.Millisecond), + Limit: ts.Limit, + Delay: float32(ts.Delay / time.Millisecond), + } +} + +// nolint: gocyclo +func testSettingsPayloadWithDefinition( + tsPayload *syntheticspb.TestSettings, ts synthetics.TestSettings, testType synthetics.TestType, +) (*syntheticspb.TestSettings, error) { + switch testType { + case synthetics.TestTypeIP: + tsPayload.Definition = ipTestDefinitionToPayload(ts) + case synthetics.TestTypeNetworkGrid: + tsPayload.Definition = networkGridTestDefinitionToPayload(ts) + case synthetics.TestTypeHostname: + tsPayload.Definition = hostnameTestDefinitionToPayload(ts) + case synthetics.TestTypeAgent: + tsPayload.Definition = agentTestDefinitionToPayload(ts) + case synthetics.TestTypeNetworkMesh: + tsPayload.Definition = networkMeshTestDefinitionToPayload(ts) + case synthetics.TestTypeFlow: + tsPayload.Definition = flowTestDefinitionToPayload(ts) + case synthetics.TestTypeURL: + tsPayload.Definition = urlTestDefinitionToPayload(ts) + case synthetics.TestTypePageLoad: + tsPayload.Definition = pageLoadTestDefinitionToPayload(ts) + case synthetics.TestTypeDNS: + tsPayload.Definition = dnsTestDefinitionToPayload(ts) + case synthetics.TestTypeDNSGrid: + tsPayload.Definition = dnsGridTestDefinitionToPayload(ts) + default: + return nil, fmt.Errorf("unsupported test type: %v", testType) + } + + return tsPayload, nil +} + +func ipTestDefinitionToPayload(ts synthetics.TestSettings) *syntheticspb.TestSettings_Ip { + return &syntheticspb.TestSettings_Ip{ + Ip: &syntheticspb.IpTest{ + Targets: convert.IPsToStrings(ts.GetIPDefinition().Targets), + }, + } +} + +func networkGridTestDefinitionToPayload(ts synthetics.TestSettings) *syntheticspb.TestSettings_NetworkGrid { + return &syntheticspb.TestSettings_NetworkGrid{ + NetworkGrid: &syntheticspb.IpTest{ + Targets: convert.IPsToStrings(ts.GetNetworkGridDefinition().Targets), + }, + } +} + +func hostnameTestDefinitionToPayload(ts synthetics.TestSettings) *syntheticspb.TestSettings_Hostname { + return &syntheticspb.TestSettings_Hostname{ + Hostname: &syntheticspb.HostnameTest{ + Target: ts.GetHostnameDefinition().Target, + }, + } +} + +func agentTestDefinitionToPayload(ts synthetics.TestSettings) *syntheticspb.TestSettings_Agent { + return &syntheticspb.TestSettings_Agent{ + Agent: &syntheticspb.AgentTest{ + Target: ts.GetAgentDefinition().Target, + UseLocalIp: ts.GetAgentDefinition().UseLocalIP, + }, + } +} + +func networkMeshTestDefinitionToPayload(ts synthetics.TestSettings) *syntheticspb.TestSettings_NetworkMesh { + return &syntheticspb.TestSettings_NetworkMesh{ + NetworkMesh: &syntheticspb.NetworkMeshTest{ + UseLocalIp: ts.GetNetworkMeshDefinition().UseLocalIP, + }, + } +} + +func flowTestDefinitionToPayload(ts synthetics.TestSettings) *syntheticspb.TestSettings_Flow { + d := ts.GetFlowDefinition() + return &syntheticspb.TestSettings_Flow{ + Flow: &syntheticspb.FlowTest{ + Target: d.Target, + TargetRefreshIntervalMillis: uint32(d.TargetRefreshInterval / time.Millisecond), + MaxProviders: d.MaxProviders, + MaxIpTargets: d.MaxIPTargets, + Type: string(d.Type), + InetDirection: string(d.InetDirection), + Direction: string(d.Direction), + }, + } +} + +func urlTestDefinitionToPayload(ts synthetics.TestSettings) *syntheticspb.TestSettings_Url { + d := ts.GetURLDefinition() + return &syntheticspb.TestSettings_Url{ + Url: &syntheticspb.UrlTest{ + Target: d.Target.String(), + Timeout: uint32(d.Timeout / time.Millisecond), + Method: d.Method, + Headers: d.Headers, + Body: d.Body, + IgnoreTlsErrors: d.IgnoreTLSErrors, + }, + } +} + +func pageLoadTestDefinitionToPayload(ts synthetics.TestSettings) *syntheticspb.TestSettings_PageLoad { + d := ts.GetPageLoadDefinition() + return &syntheticspb.TestSettings_PageLoad{ + PageLoad: &syntheticspb.PageLoadTest{ + Target: d.Target.String(), + Timeout: uint32(d.Timeout / time.Millisecond), + Headers: d.Headers, + IgnoreTlsErrors: d.IgnoreTLSErrors, + CssSelectors: d.CSSSelectors, + }, + } +} + +func dnsTestDefinitionToPayload(ts synthetics.TestSettings) *syntheticspb.TestSettings_Dns { + return &syntheticspb.TestSettings_Dns{ + Dns: dnsTestSubDefinitionToPayload(ts.GetDNSDefinition()), + } +} + +func dnsGridTestDefinitionToPayload(ts synthetics.TestSettings) *syntheticspb.TestSettings_DnsGrid { + return &syntheticspb.TestSettings_DnsGrid{ + DnsGrid: dnsTestSubDefinitionToPayload(ts.GetDNSGridDefinition()), + } +} + +func dnsTestSubDefinitionToPayload(d *synthetics.TestDefinitionDNS) *syntheticspb.DnsTest { + return &syntheticspb.DnsTest{ + Target: d.Target, + Timeout: uint32(d.Timeout / time.Millisecond), + RecordType: syntheticspb.DNSRecord(syntheticspb.DNSRecord_value[string(d.RecordType)]), + Servers: convert.IPsToStrings(d.Servers), + Port: d.Port, + } +} diff --git a/kentikapi/synth_spy_server_test.go b/kentikapi/synth_spy_server_test.go index 75a28d1..92d281a 100644 --- a/kentikapi/synth_spy_server_test.go +++ b/kentikapi/synth_spy_server_test.go @@ -28,10 +28,16 @@ type spySyntheticsServer struct { } type syntheticsRequests struct { - listAgentsRequests []listAgentsRequest - getAgentRequests []getAgentRequest - updateAgentRequests []updateAgentRequest - deleteAgentRequests []deleteAgentRequest + listAgentsRequests []listAgentsRequest + getAgentRequests []getAgentRequest + updateAgentRequests []updateAgentRequest + deleteAgentRequests []deleteAgentRequest + listTestsRequests []listTestsRequest + getTestRequests []getTestRequest + createTestRequests []createTestRequest + updateTestRequests []updateTestRequest + deleteTestRequests []deleteTestRequest + setTestStatusRequests []setTestStatusRequest } type listAgentsRequest struct { @@ -54,11 +60,47 @@ type deleteAgentRequest struct { data *synthetics.DeleteAgentRequest } +type listTestsRequest struct { + metadata metadata.MD + data *synthetics.ListTestsRequest +} + +type getTestRequest struct { + metadata metadata.MD + data *synthetics.GetTestRequest +} + +type createTestRequest struct { + metadata metadata.MD + data *synthetics.CreateTestRequest +} + +type updateTestRequest struct { + metadata metadata.MD + data *synthetics.UpdateTestRequest +} + +type deleteTestRequest struct { + metadata metadata.MD + data *synthetics.DeleteTestRequest +} + +type setTestStatusRequest struct { + metadata metadata.MD + data *synthetics.SetTestStatusRequest +} + type syntheticsResponses struct { - listAgentsResponse listAgentsResponse - getAgentResponse getAgentResponse - updateAgentResponse updateAgentResponse - deleteAgentResponse deleteAgentResponse + listAgentsResponse listAgentsResponse + getAgentResponse getAgentResponse + updateAgentResponse updateAgentResponse + deleteAgentResponse deleteAgentResponse + listTestsResponse listTestsResponse + getTestResponse getTestResponse + createTestResponse createTestResponse + updateTestResponse updateTestResponse + deleteTestResponse deleteTestResponse + setTestStatusResponse setTestStatusResponse } type listAgentsResponse struct { @@ -81,6 +123,36 @@ type deleteAgentResponse struct { err error } +type listTestsResponse struct { + data *synthetics.ListTestsResponse + err error +} + +type getTestResponse struct { + data *synthetics.GetTestResponse + err error +} + +type createTestResponse struct { + data *synthetics.CreateTestResponse + err error +} + +type updateTestResponse struct { + data *synthetics.UpdateTestResponse + err error +} + +type deleteTestResponse struct { + data *synthetics.DeleteTestResponse + err error +} + +type setTestStatusResponse struct { + data *synthetics.SetTestStatusResponse + err error +} + func newSpySyntheticsServer(t testing.TB, r syntheticsResponses) *spySyntheticsServer { return &spySyntheticsServer{ done: make(chan struct{}), @@ -159,3 +231,75 @@ func (s *spySyntheticsServer) DeleteAgent( return s.responses.deleteAgentResponse.data, s.responses.deleteAgentResponse.err } + +func (s *spySyntheticsServer) ListTests( + ctx context.Context, req *synthetics.ListTestsRequest, +) (*synthetics.ListTestsResponse, error) { + md, _ := metadata.FromIncomingContext(ctx) + s.requests.listTestsRequests = append(s.requests.listTestsRequests, listTestsRequest{ + metadata: md, + data: req, + }) + + return s.responses.listTestsResponse.data, s.responses.listTestsResponse.err +} + +func (s *spySyntheticsServer) GetTest( + ctx context.Context, req *synthetics.GetTestRequest, +) (*synthetics.GetTestResponse, error) { + md, _ := metadata.FromIncomingContext(ctx) + s.requests.getTestRequests = append(s.requests.getTestRequests, getTestRequest{ + metadata: md, + data: req, + }) + + return s.responses.getTestResponse.data, s.responses.getTestResponse.err +} + +func (s *spySyntheticsServer) CreateTest( + ctx context.Context, req *synthetics.CreateTestRequest, +) (*synthetics.CreateTestResponse, error) { + md, _ := metadata.FromIncomingContext(ctx) + s.requests.createTestRequests = append(s.requests.createTestRequests, createTestRequest{ + metadata: md, + data: req, + }) + + return s.responses.createTestResponse.data, s.responses.createTestResponse.err +} + +func (s *spySyntheticsServer) UpdateTest( + ctx context.Context, req *synthetics.UpdateTestRequest, +) (*synthetics.UpdateTestResponse, error) { + md, _ := metadata.FromIncomingContext(ctx) + s.requests.updateTestRequests = append(s.requests.updateTestRequests, updateTestRequest{ + metadata: md, + data: req, + }) + + return s.responses.updateTestResponse.data, s.responses.updateTestResponse.err +} + +func (s *spySyntheticsServer) DeleteTest( + ctx context.Context, req *synthetics.DeleteTestRequest, +) (*synthetics.DeleteTestResponse, error) { + md, _ := metadata.FromIncomingContext(ctx) + s.requests.deleteTestRequests = append(s.requests.deleteTestRequests, deleteTestRequest{ + metadata: md, + data: req, + }) + + return s.responses.deleteTestResponse.data, s.responses.deleteTestResponse.err +} + +func (s *spySyntheticsServer) SetTestStatus( + ctx context.Context, req *synthetics.SetTestStatusRequest, +) (*synthetics.SetTestStatusResponse, error) { + md, _ := metadata.FromIncomingContext(ctx) + s.requests.setTestStatusRequests = append(s.requests.setTestStatusRequests, setTestStatusRequest{ + metadata: md, + data: req, + }) + + return s.responses.setTestStatusResponse.data, s.responses.setTestStatusResponse.err +} diff --git a/kentikapi/synth_test_integration_test.go b/kentikapi/synth_test_integration_test.go new file mode 100644 index 0000000..be526a7 --- /dev/null +++ b/kentikapi/synth_test_integration_test.go @@ -0,0 +1,1453 @@ +package kentikapi_test + +import ( + "context" + "net" + "net/http" + "net/url" + "testing" + "time" + + "github.com/AlekSi/pointer" + syntheticspb "github.com/kentik/api-schema-public/gen/go/kentik/synthetics/v202202" + "github.com/kentik/community_sdk_golang/kentikapi" + "github.com/kentik/community_sdk_golang/kentikapi/internal/testutil" + "github.com/kentik/community_sdk_golang/kentikapi/models" + "github.com/kentik/community_sdk_golang/kentikapi/synthetics" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/timestamppb" +) + +const ( + ipTestID = "1001" + networkGridTestID = "1002" + hostnameTestID = "1003" + agentTestID = "1004" + networkMeshTestID = "1005" + flowTestID = "1006" + urlTestID = "1007" + pageLoadTestID = "1008" + dnsTestID = "1009" + dnsGridTestID = "1010" + bgpMonitorTestID = "1011" + transactionTestID = "1012" +) + +func TestClient_Synthetics_GetAllTests(t *testing.T) { + tests := []struct { + name string + response listTestsResponse + expectedResult *synthetics.GetAllTestsResponse + expectedError bool + errorPredicates []func(error) bool + }{ + { + name: "status InvalidArgument received", + response: listTestsResponse{ + err: status.Errorf(codes.InvalidArgument, codes.InvalidArgument.String()), + }, + expectedError: true, + errorPredicates: []func(error) bool{kentikapi.IsInvalidRequestError}, + }, { + name: "empty response received", + response: listTestsResponse{ + data: &syntheticspb.ListTestsResponse{}, + }, + expectedResult: &synthetics.GetAllTestsResponse{ + Tests: nil, + InvalidTestsCount: 0, + }, + }, { + name: "no tests received", + response: listTestsResponse{ + data: &syntheticspb.ListTestsResponse{ + Tests: []*syntheticspb.Test{}, + InvalidCount: 0, + }, + }, + expectedResult: &synthetics.GetAllTestsResponse{ + Tests: nil, + InvalidTestsCount: 0, + }, + }, { + name: "multiple tests received", + response: listTestsResponse{ + data: &syntheticspb.ListTestsResponse{ + Tests: []*syntheticspb.Test{ + newIPTestPayload(), + newNetworkGridTestPayload(), + newHostnameTestPayload(), + newAgentTestPayload(), + newNetworkMeshTestPayload(), + newFlowTestPayload(), + newURLTestPayload(), + newPageLoadTestPayload(), + newDNSTestPayload(), + newDNSGridTestPayload(), + newBGPMonitorTestPayload(), + newTransactionTestPayload(), + }, + InvalidCount: 1, + }, + }, + expectedResult: &synthetics.GetAllTestsResponse{ + Tests: []synthetics.Test{ + *newIPTest(), + *newNetworkGridTest(), + *newHostnameTest(), + *newAgentTest(), + *newNetworkMeshTest(), + *newFlowTest(), + *newURLTest(), + *newPageLoadTest(), + *newDNSTest(), + *newDNSGridTest(), + // BGP monitor test should be silently ignored + // transaction test should be silently ignored + }, + InvalidTestsCount: 1, + }, + }, { + name: "2 tests received - one nil", + response: listTestsResponse{ + data: &syntheticspb.ListTestsResponse{ + Tests: []*syntheticspb.Test{ + newIPTestPayload(), + nil, + }, + InvalidCount: 0, + }, + }, + expectedError: true, // InvalidResponse + }, + } + //nolint:dupl + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // arrange + server := newSpySyntheticsServer(t, syntheticsResponses{ + listTestsResponse: tt.response, + }) + server.Start() + defer server.Stop() + + client, err := kentikapi.NewClient( + kentikapi.WithAPIURL("http://"+server.url), + kentikapi.WithCredentials(dummyAuthEmail, dummyAuthToken), + kentikapi.WithLogPayloads(), + ) + require.NoError(t, err) + + // act + result, err := client.Synthetics.GetAllTests(context.Background()) + + // assert + t.Logf("Got result: %+v, err: %v", result, err) + if tt.expectedError { + assert.Error(t, err) + for _, isErr := range tt.errorPredicates { + assert.True(t, isErr(err)) + } + } else { + assert.NoError(t, err) + } + + if assert.Equal(t, 1, len(server.requests.listTestsRequests), "invalid number of requests") { + r := server.requests.listTestsRequests[0] + assert.Equal(t, dummyAuthEmail, r.metadata.Get(authEmailKey)[0]) + assert.Equal(t, dummyAuthToken, r.metadata.Get(authAPITokenKey)[0]) + testutil.AssertProtoEqual(t, &syntheticspb.ListTestsRequest{}, r.data) + } + + assert.Equal(t, tt.expectedResult, result) + }) + } +} + +func TestClient_Synthetics_GetTest(t *testing.T) { + tests := []struct { + name string + requestID models.ID + expectedRequest *syntheticspb.GetTestRequest + response getTestResponse + expectedResult *synthetics.Test + expectedError bool + errorPredicates []func(error) bool + }{ + { + name: "status InvalidArgument received", + requestID: "13", + expectedRequest: &syntheticspb.GetTestRequest{Id: "13"}, + response: getTestResponse{ + err: status.Errorf(codes.InvalidArgument, codes.InvalidArgument.String()), + }, + expectedError: true, + errorPredicates: []func(error) bool{kentikapi.IsInvalidRequestError}, + }, { + name: "status NotFound received", + requestID: "13", + expectedRequest: &syntheticspb.GetTestRequest{Id: "13"}, + response: getTestResponse{ + err: status.Errorf(codes.NotFound, codes.NotFound.String()), + }, + expectedError: true, + errorPredicates: []func(error) bool{kentikapi.IsNotFoundError}, + }, { + name: "empty response received", + requestID: "13", + expectedRequest: &syntheticspb.GetTestRequest{Id: "13"}, + response: getTestResponse{ + data: &syntheticspb.GetTestResponse{}, + }, + expectedError: true, // InvalidResponse + }, { + name: "ip test returned", + requestID: ipTestID, + expectedRequest: &syntheticspb.GetTestRequest{Id: ipTestID}, + response: getTestResponse{ + data: &syntheticspb.GetTestResponse{Test: newIPTestPayload()}, + }, + expectedResult: newIPTest(), + }, { + name: "network grid test returned", + requestID: networkGridTestID, + expectedRequest: &syntheticspb.GetTestRequest{Id: networkGridTestID}, + response: getTestResponse{ + data: &syntheticspb.GetTestResponse{Test: newNetworkGridTestPayload()}, + }, + expectedResult: newNetworkGridTest(), + }, { + name: "hostname test returned", + requestID: hostnameTestID, + expectedRequest: &syntheticspb.GetTestRequest{Id: hostnameTestID}, + response: getTestResponse{ + data: &syntheticspb.GetTestResponse{Test: newHostnameTestPayload()}, + }, + expectedResult: newHostnameTest(), + }, { + name: "agent test returned", + requestID: agentTestID, + expectedRequest: &syntheticspb.GetTestRequest{Id: agentTestID}, + response: getTestResponse{ + data: &syntheticspb.GetTestResponse{Test: newAgentTestPayload()}, + }, + expectedResult: newAgentTest(), + }, { + name: "network mesh test returned", + requestID: networkMeshTestID, + expectedRequest: &syntheticspb.GetTestRequest{Id: networkMeshTestID}, + response: getTestResponse{ + data: &syntheticspb.GetTestResponse{Test: newNetworkMeshTestPayload()}, + }, + expectedResult: newNetworkMeshTest(), + }, { + name: "flow test returned", + requestID: flowTestID, + expectedRequest: &syntheticspb.GetTestRequest{Id: flowTestID}, + response: getTestResponse{ + data: &syntheticspb.GetTestResponse{Test: newFlowTestPayload()}, + }, + expectedResult: newFlowTest(), + }, { + name: "URL test returned", + requestID: urlTestID, + expectedRequest: &syntheticspb.GetTestRequest{Id: urlTestID}, + response: getTestResponse{ + data: &syntheticspb.GetTestResponse{Test: newURLTestPayload()}, + }, + expectedResult: newURLTest(), + }, { + name: "page load test returned", + requestID: pageLoadTestID, + expectedRequest: &syntheticspb.GetTestRequest{Id: pageLoadTestID}, + response: getTestResponse{ + data: &syntheticspb.GetTestResponse{Test: newPageLoadTestPayload()}, + }, + expectedResult: newPageLoadTest(), + }, { + name: "DNS test returned", + requestID: dnsTestID, + expectedRequest: &syntheticspb.GetTestRequest{Id: dnsTestID}, + response: getTestResponse{ + data: &syntheticspb.GetTestResponse{Test: newDNSTestPayload()}, + }, + expectedResult: newDNSTest(), + }, { + name: "DNS grid test returned", + requestID: dnsGridTestID, + expectedRequest: &syntheticspb.GetTestRequest{Id: dnsGridTestID}, + response: getTestResponse{ + data: &syntheticspb.GetTestResponse{Test: newDNSGridTestPayload()}, + }, + expectedResult: newDNSGridTest(), + }, { + name: "BGP monitor test returned", + requestID: bgpMonitorTestID, + expectedRequest: &syntheticspb.GetTestRequest{Id: bgpMonitorTestID}, + response: getTestResponse{ + data: &syntheticspb.GetTestResponse{Test: newBGPMonitorTestPayload()}, + }, + expectedResult: nil, + expectedError: true, // InvalidResponse + }, { + name: "transaction test returned", + requestID: transactionTestID, + expectedRequest: &syntheticspb.GetTestRequest{Id: transactionTestID}, + response: getTestResponse{ + data: &syntheticspb.GetTestResponse{Test: newTransactionTestPayload()}, + }, + expectedResult: nil, + expectedError: true, // InvalidResponse + }, + } + //nolint:dupl + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // arrange + server := newSpySyntheticsServer(t, syntheticsResponses{ + getTestResponse: tt.response, + }) + server.Start() + defer server.Stop() + + client, err := kentikapi.NewClient( + kentikapi.WithAPIURL("http://"+server.url), + kentikapi.WithCredentials(dummyAuthEmail, dummyAuthToken), + kentikapi.WithLogPayloads(), + ) + require.NoError(t, err) + + // act + result, err := client.Synthetics.GetTest(context.Background(), tt.requestID) + + // assert + t.Logf("Got result: %+v, err: %v", result, err) + if tt.expectedError { + assert.Error(t, err) + for _, isErr := range tt.errorPredicates { + assert.True(t, isErr(err)) + } + } else { + assert.NoError(t, err) + } + + if assert.Equal(t, 1, len(server.requests.getTestRequests), "invalid number of requests") { + r := server.requests.getTestRequests[0] + assert.Equal(t, dummyAuthEmail, r.metadata.Get(authEmailKey)[0]) + assert.Equal(t, dummyAuthToken, r.metadata.Get(authAPITokenKey)[0]) + testutil.AssertProtoEqual(t, tt.expectedRequest, r.data) + } + + assert.Equal(t, tt.expectedResult, result) + }) + } +} + +func TestClient_Synthetics_CreateTest(t *testing.T) { + tests := []struct { + name string + request *synthetics.Test + expectedRequest *syntheticspb.CreateTestRequest + response createTestResponse + expectedResult *synthetics.Test + expectedError bool + errorPredicates []func(error) bool + }{ + { + name: "nil request", + request: nil, + expectedRequest: nil, + expectedResult: nil, + expectedError: true, + errorPredicates: []func(error) bool{kentikapi.IsInvalidRequestError}, + }, { + name: "empty response received", + request: newIPTest(), + expectedRequest: &syntheticspb.CreateTestRequest{ + Test: testWithoutReadOnlyFields(newIPTestPayload()), + }, + response: createTestResponse{ + data: &syntheticspb.CreateTestResponse{Test: nil}, + }, + expectedResult: nil, + expectedError: true, // InvalidResponse + }, { + name: "minimal hostname test created", + request: synthetics.NewHostnameTest(synthetics.HostnameTestRequiredFields{ + BasePingTraceTestRequiredFields: synthetics.BasePingTraceTestRequiredFields{ + BaseTestRequiredFields: synthetics.BaseTestRequiredFields{ + Name: "minimal-hostname-test", + AgentIDs: []string{"817", "818", "819"}, + }, + Ping: synthetics.PingSettingsRequiredFields{ + Timeout: 10 * time.Second, + Count: 10, + Protocol: synthetics.PingProtocolTCP, + Port: 65535, + }, + Traceroute: synthetics.TracerouteSettingsRequiredFields{ + Timeout: 59999 * time.Millisecond, + Count: 5, + Delay: 100 * time.Millisecond, + Protocol: synthetics.TracerouteProtocolUDP, + Limit: 255, + }, + }, + Definition: synthetics.TestDefinitionHostnameRequiredFields{ + Target: "www.example.com", + }, + }), + expectedRequest: &syntheticspb.CreateTestRequest{ + Test: &syntheticspb.Test{ + Name: "minimal-hostname-test", + Type: "hostname", + Status: syntheticspb.TestStatus_TEST_STATUS_ACTIVE, + Settings: &syntheticspb.TestSettings{ + Definition: &syntheticspb.TestSettings_Hostname{ + Hostname: &syntheticspb.HostnameTest{ + Target: "www.example.com", + }, + }, + AgentIds: []string{"817", "818", "819"}, + Tasks: []string{"ping", "traceroute"}, + HealthSettings: &syntheticspb.HealthSettings{ + UnhealthySubtestThreshold: 1, + }, + Ping: &syntheticspb.TestPingSettings{ + Count: 10, + Protocol: "tcp", + Port: 65535, + Timeout: 10000, + Delay: 0, + }, + Trace: &syntheticspb.TestTraceSettings{ + Count: 5, + Protocol: "udp", + Port: 33434, + Timeout: 59999, + Limit: 255, + Delay: 100, + }, + Period: 60, + Family: syntheticspb.IPFamily_IP_FAMILY_DUAL, + }, + }, + }, + response: createTestResponse{ + // a minimal hostname test data returned by Kentik API + data: &syntheticspb.CreateTestResponse{ + Test: &syntheticspb.Test{ + Name: "minimal-hostname-test", + Type: "hostname", + Status: syntheticspb.TestStatus_TEST_STATUS_ACTIVE, + Settings: &syntheticspb.TestSettings{ + Definition: &syntheticspb.TestSettings_Hostname{ + Hostname: &syntheticspb.HostnameTest{ + Target: "www.example.com", + }, + }, + AgentIds: []string{"817", "818", "819"}, + Tasks: []string{"ping", "traceroute"}, + HealthSettings: &syntheticspb.HealthSettings{ + UnhealthySubtestThreshold: 1, + Activation: &syntheticspb.ActivationSettings{ + GracePeriod: "2", + TimeUnit: "m", + TimeWindow: "5", + Times: "3", + }, + }, + Ping: &syntheticspb.TestPingSettings{ + Count: 10, + Protocol: "tcp", + Port: 65535, + Timeout: 10000, + Delay: 0, + }, + Trace: &syntheticspb.TestTraceSettings{ + Count: 5, + Protocol: "udp", + Port: 33434, + Timeout: 59999, + Limit: 255, + Delay: 100, + }, + Period: 60, + Family: syntheticspb.IPFamily_IP_FAMILY_DUAL, + }, + Id: hostnameTestID, + Cdate: timestamppb.New(time.Date(2022, time.April, 6, 9, 43, 39, 324*1000000, time.UTC)), + Edate: timestamppb.New(time.Date(2022, time.April, 6, 9, 43, 39, 835*1000000, time.UTC)), + CreatedBy: &syntheticspb.UserInfo{ + Id: "4321", + Email: "joe.doe@example.com", + FullName: "Joe Doe", + }, + LastUpdatedBy: nil, + }, + }, + }, + expectedResult: &synthetics.Test{ + Name: "minimal-hostname-test", + Type: synthetics.TestTypeHostname, + Status: synthetics.TestStatusActive, + UpdateDate: pointer.ToTime(time.Date(2022, time.April, 6, 9, 43, 39, 835*1000000, time.UTC)), + Settings: synthetics.TestSettings{ + Definition: &synthetics.TestDefinitionHostname{ + Target: "www.example.com", + }, + AgentIDs: []string{"817", "818", "819"}, + Period: 60 * time.Second, + Family: synthetics.IPFamilyDual, + Health: synthetics.HealthSettings{ + UnhealthySubtestThreshold: 1, + AlarmActivation: &synthetics.AlarmActivationSettings{ + TimeWindow: 5 * time.Minute, + Times: 3, + GracePeriod: 2, + }, + }, + Ping: &synthetics.PingSettings{ + Timeout: 10 * time.Second, + Count: 10, + Delay: 0, + Protocol: synthetics.PingProtocolTCP, + Port: 65535, + }, + Traceroute: &synthetics.TracerouteSettings{ + Timeout: 59999 * time.Millisecond, + Count: 5, + Delay: 100 * time.Millisecond, + Protocol: synthetics.TracerouteProtocolUDP, + Port: 33434, + Limit: 255, + }, + Tasks: []synthetics.TaskType{synthetics.TaskTypePing, synthetics.TaskTypeTraceroute}, + }, + ID: hostnameTestID, + CreateDate: time.Date(2022, time.April, 6, 9, 43, 39, 324*1000000, time.UTC), + CreatedBy: synthetics.UserInfo{ + ID: "4321", + Email: "joe.doe@example.com", + FullName: "Joe Doe", + }, + LastUpdatedBy: nil, + }, + }, { + name: "IP test created", + request: newIPTest(), + expectedRequest: &syntheticspb.CreateTestRequest{ + Test: testWithoutReadOnlyFields(newIPTestPayload()), + }, + response: createTestResponse{ + data: &syntheticspb.CreateTestResponse{ + Test: newIPTestPayload(), + }, + }, + expectedResult: newIPTest(), + }, { + name: "network grid test created", + request: newNetworkGridTest(), + expectedRequest: &syntheticspb.CreateTestRequest{ + Test: testWithoutReadOnlyFields(newNetworkGridTestPayload()), + }, + response: createTestResponse{ + data: &syntheticspb.CreateTestResponse{ + Test: newNetworkGridTestPayload(), + }, + }, + expectedResult: newNetworkGridTest(), + }, { + name: "hostname test created", + request: newHostnameTest(), + expectedRequest: &syntheticspb.CreateTestRequest{ + Test: testWithoutReadOnlyFields(newHostnameTestPayload()), + }, + response: createTestResponse{ + data: &syntheticspb.CreateTestResponse{ + Test: newHostnameTestPayload(), + }, + }, + expectedResult: newHostnameTest(), + }, { + name: "agent test created", + request: newAgentTest(), + expectedRequest: &syntheticspb.CreateTestRequest{ + Test: testWithoutReadOnlyFields(newAgentTestPayload()), + }, + response: createTestResponse{ + data: &syntheticspb.CreateTestResponse{ + Test: newAgentTestPayload(), + }, + }, + expectedResult: newAgentTest(), + }, { + name: "network mesh test created", + request: newNetworkMeshTest(), + expectedRequest: &syntheticspb.CreateTestRequest{ + Test: testWithoutReadOnlyFields(newNetworkMeshTestPayload()), + }, + response: createTestResponse{ + data: &syntheticspb.CreateTestResponse{ + Test: newNetworkMeshTestPayload(), + }, + }, + expectedResult: newNetworkMeshTest(), + }, { + name: "flow test created", + request: newFlowTest(), + expectedRequest: &syntheticspb.CreateTestRequest{ + Test: testWithoutReadOnlyFields(newFlowTestPayload()), + }, + response: createTestResponse{ + data: &syntheticspb.CreateTestResponse{ + Test: newFlowTestPayload(), + }, + }, + expectedResult: newFlowTest(), + }, { + name: "URL test created", + request: newURLTest(), + expectedRequest: &syntheticspb.CreateTestRequest{ + Test: testWithoutReadOnlyFields(newURLTestPayload()), + }, + response: createTestResponse{ + data: &syntheticspb.CreateTestResponse{ + Test: newURLTestPayload(), + }, + }, + expectedResult: newURLTest(), + }, { + name: "page load test created", + request: newPageLoadTest(), + expectedRequest: &syntheticspb.CreateTestRequest{ + Test: testWithoutReadOnlyFields(newPageLoadTestPayload()), + }, + response: createTestResponse{ + data: &syntheticspb.CreateTestResponse{ + Test: newPageLoadTestPayload(), + }, + }, + expectedResult: newPageLoadTest(), + }, { + name: "DNS test created", + request: newDNSTest(), + expectedRequest: &syntheticspb.CreateTestRequest{ + Test: testWithoutReadOnlyFields(newDNSTestPayload()), + }, + response: createTestResponse{ + data: &syntheticspb.CreateTestResponse{ + Test: newDNSTestPayload(), + }, + }, + expectedResult: newDNSTest(), + }, { + name: "DNS grid test created", + request: newDNSGridTest(), + expectedRequest: &syntheticspb.CreateTestRequest{ + Test: testWithoutReadOnlyFields(newDNSGridTestPayload()), + }, + response: createTestResponse{ + data: &syntheticspb.CreateTestResponse{ + Test: newDNSGridTestPayload(), + }, + }, + expectedResult: newDNSGridTest(), + }, { + name: "BGP monitor test passed", + request: newBGPMonitorTest(), + expectedRequest: nil, + expectedResult: nil, + expectedError: true, + errorPredicates: []func(error) bool{kentikapi.IsInvalidRequestError}, + }, { + name: "transaction test passed", + request: newTransactionTest(), + expectedRequest: nil, + expectedResult: nil, + expectedError: true, + errorPredicates: []func(error) bool{kentikapi.IsInvalidRequestError}, + }, + } + //nolint:dupl + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // arrange + server := newSpySyntheticsServer(t, syntheticsResponses{ + createTestResponse: tt.response, + }) + server.Start() + defer server.Stop() + + client, err := kentikapi.NewClient( + kentikapi.WithAPIURL("http://"+server.url), + kentikapi.WithCredentials(dummyAuthEmail, dummyAuthToken), + kentikapi.WithLogPayloads(), + ) + require.NoError(t, err) + + // act + result, err := client.Synthetics.CreateTest(context.Background(), tt.request) + + // assert + t.Logf("Got result: %+v, err: %v", result, err) + if tt.expectedError { + assert.Error(t, err) + for _, isErr := range tt.errorPredicates { + assert.True(t, isErr(err)) + } + } else { + assert.NoError(t, err) + } + + if tt.expectedRequest != nil && assert.Equal( + t, 1, len(server.requests.createTestRequests), "invalid number of requests", + ) { + r := server.requests.createTestRequests[0] + assert.Equal(t, dummyAuthEmail, r.metadata.Get(authEmailKey)[0]) + assert.Equal(t, dummyAuthToken, r.metadata.Get(authAPITokenKey)[0]) + testutil.AssertProtoEqual(t, tt.expectedRequest, r.data) + } + assert.Equal(t, tt.expectedResult, result) + }) + } +} + +func TestClient_Synthetics_UpdateTest(t *testing.T) { + tests := []struct { + name string + request *synthetics.Test + expectedRequest *syntheticspb.UpdateTestRequest + response updateTestResponse + expectedResult *synthetics.Test + expectedError bool + errorPredicates []func(error) bool + }{ + { + name: "nil request", + request: nil, + expectedRequest: nil, + expectedResult: nil, + expectedError: true, + errorPredicates: []func(error) bool{kentikapi.IsInvalidRequestError}, + }, { + name: "empty response received", + request: newHostnameTest(), + expectedRequest: &syntheticspb.UpdateTestRequest{ + Test: testWithoutReadOnlyFields(newHostnameTestPayload()), + }, + response: updateTestResponse{ + data: &syntheticspb.UpdateTestResponse{Test: nil}, + }, + expectedResult: nil, + expectedError: true, // InvalidResponse + }, { + name: "test updated", + request: newHostnameTest(), + expectedRequest: &syntheticspb.UpdateTestRequest{ + Test: testWithoutReadOnlyFields(newHostnameTestPayload()), + }, + response: updateTestResponse{ + data: &syntheticspb.UpdateTestResponse{ + Test: newHostnameTestPayload(), + }, + }, + expectedResult: newHostnameTest(), + }, { + name: "BGP monitor test passed", + request: newBGPMonitorTest(), + expectedRequest: nil, + expectedResult: nil, + expectedError: true, + errorPredicates: []func(error) bool{kentikapi.IsInvalidRequestError}, + }, { + name: "transaction test passed", + request: newTransactionTest(), + expectedRequest: nil, + expectedResult: nil, + expectedError: true, + errorPredicates: []func(error) bool{kentikapi.IsInvalidRequestError}, + }, + } + // nolint: dupl + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // arrange + server := newSpySyntheticsServer(t, syntheticsResponses{ + updateTestResponse: tt.response, + }) + server.Start() + defer server.Stop() + + client, err := kentikapi.NewClient( + kentikapi.WithAPIURL("http://"+server.url), + kentikapi.WithCredentials(dummyAuthEmail, dummyAuthToken), + kentikapi.WithLogPayloads(), + ) + require.NoError(t, err) + + // act + result, err := client.Synthetics.UpdateTest(context.Background(), tt.request) + + // assert + t.Logf("Got result: %+v, err: %v", result, err) + if tt.expectedError { + assert.Error(t, err) + for _, isErr := range tt.errorPredicates { + assert.True(t, isErr(err)) + } + } else { + assert.NoError(t, err) + } + + if tt.expectedRequest != nil && assert.Equal( + t, 1, len(server.requests.updateTestRequests), "invalid number of requests", + ) { + r := server.requests.updateTestRequests[0] + assert.Equal(t, dummyAuthEmail, r.metadata.Get(authEmailKey)[0]) + assert.Equal(t, dummyAuthToken, r.metadata.Get(authAPITokenKey)[0]) + testutil.AssertProtoEqual(t, tt.expectedRequest, r.data) + } + + assert.Equal(t, tt.expectedResult, result) + }) + } +} + +func TestClient_Synthetics_DeleteTest(t *testing.T) { + tests := []struct { + name string + requestID string + expectedRequest *syntheticspb.DeleteTestRequest + response deleteTestResponse + expectedError bool + errorPredicates []func(error) bool + }{ + { + name: "status InvalidArgument received", + requestID: "13", + expectedRequest: &syntheticspb.DeleteTestRequest{Id: "13"}, + response: deleteTestResponse{ + data: &syntheticspb.DeleteTestResponse{}, + err: status.Errorf(codes.InvalidArgument, codes.InvalidArgument.String()), + }, + expectedError: true, + errorPredicates: []func(error) bool{kentikapi.IsInvalidRequestError}, + }, { + name: "resource deleted", + requestID: "13", + expectedRequest: &syntheticspb.DeleteTestRequest{Id: "13"}, + response: deleteTestResponse{ + data: &syntheticspb.DeleteTestResponse{}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // arrange + server := newSpySyntheticsServer(t, syntheticsResponses{ + deleteTestResponse: tt.response, + }) + server.Start() + defer server.Stop() + + client, err := kentikapi.NewClient( + kentikapi.WithAPIURL("http://"+server.url), + kentikapi.WithCredentials(dummyAuthEmail, dummyAuthToken), + kentikapi.WithLogPayloads(), + ) + require.NoError(t, err) + + // act + err = client.Synthetics.DeleteTest(context.Background(), tt.requestID) + + // assert + t.Logf("Got err: %v", err) + if tt.expectedError { + assert.Error(t, err) + for _, isErr := range tt.errorPredicates { + assert.True(t, isErr(err)) + } + } else { + assert.NoError(t, err) + } + + if assert.Equal(t, 1, len(server.requests.deleteTestRequests), "invalid number of requests") { + r := server.requests.deleteTestRequests[0] + assert.Equal(t, dummyAuthEmail, r.metadata.Get(authEmailKey)[0]) + assert.Equal(t, dummyAuthToken, r.metadata.Get(authAPITokenKey)[0]) + testutil.AssertProtoEqual(t, tt.expectedRequest, r.data) + } + }) + } +} + +func TestClient_Synthetics_SetTestStatus(t *testing.T) { + tests := []struct { + name string + requestID string + requestStatus synthetics.TestStatus + expectedRequest *syntheticspb.SetTestStatusRequest + response setTestStatusResponse + expectedError bool + errorPredicates []func(error) bool + }{ + { + name: "status InvalidArgument received", + requestID: "13", + requestStatus: synthetics.TestStatusDeleted, + expectedRequest: &syntheticspb.SetTestStatusRequest{ + Id: "13", + Status: syntheticspb.TestStatus_TEST_STATUS_DELETED, + }, + response: setTestStatusResponse{ + data: &syntheticspb.SetTestStatusResponse{}, + err: status.Errorf(codes.InvalidArgument, codes.InvalidArgument.String()), + }, + expectedError: true, + errorPredicates: []func(error) bool{kentikapi.IsInvalidRequestError}, + }, { + name: "status set", + requestID: "13", + requestStatus: synthetics.TestStatusPaused, + expectedRequest: &syntheticspb.SetTestStatusRequest{ + Id: "13", + Status: syntheticspb.TestStatus_TEST_STATUS_PAUSED, + }, + response: setTestStatusResponse{ + data: &syntheticspb.SetTestStatusResponse{}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // arrange + server := newSpySyntheticsServer(t, syntheticsResponses{ + setTestStatusResponse: tt.response, + }) + server.Start() + defer server.Stop() + + client, err := kentikapi.NewClient( + kentikapi.WithAPIURL("http://"+server.url), + kentikapi.WithCredentials(dummyAuthEmail, dummyAuthToken), + kentikapi.WithLogPayloads(), + ) + require.NoError(t, err) + + // act + err = client.Synthetics.SetTestStatus(context.Background(), tt.requestID, tt.requestStatus) + + // assert + t.Logf("Got err: %v", err) + if tt.expectedError { + assert.Error(t, err) + for _, isErr := range tt.errorPredicates { + assert.True(t, isErr(err)) + } + } else { + assert.NoError(t, err) + } + + if assert.Equal(t, 1, len(server.requests.setTestStatusRequests), "invalid number of requests") { + r := server.requests.setTestStatusRequests[0] + assert.Equal(t, dummyAuthEmail, r.metadata.Get(authEmailKey)[0]) + assert.Equal(t, dummyAuthToken, r.metadata.Get(authAPITokenKey)[0]) + testutil.AssertProtoEqual(t, tt.expectedRequest, r.data) + } + }) + } +} + +func testWithoutReadOnlyFields(test *syntheticspb.Test) *syntheticspb.Test { + test.Cdate = nil + test.CreatedBy = nil + test.LastUpdatedBy = nil + return test +} + +func newIPTest() *synthetics.Test { + t := newTest() + t.Name = "ip-test" + t.Type = synthetics.TestTypeIP + t.ID = ipTestID + t.Settings.Definition = &synthetics.TestDefinitionIP{ + Targets: []net.IP{net.ParseIP("192.0.2.213"), net.ParseIP("2001:db8:dead:beef:dead:beef:dead:beef")}, + } + return t +} + +func newIPTestPayload() *syntheticspb.Test { + t := newTestPayload() + t.Name = "ip-test" + t.Type = "ip" + t.Id = ipTestID + t.Settings.Definition = &syntheticspb.TestSettings_Ip{ + Ip: &syntheticspb.IpTest{ + Targets: []string{"192.0.2.213", "2001:db8:dead:beef:dead:beef:dead:beef"}, + }, + } + return t +} + +func newNetworkGridTest() *synthetics.Test { + t := newTest() + t.Name = "network-grid-test" + t.Type = synthetics.TestTypeNetworkGrid + t.ID = networkGridTestID + t.Settings.Definition = &synthetics.TestDefinitionNetworkGrid{ + Targets: []net.IP{net.ParseIP("192.0.2.213"), net.ParseIP("2001:db8:dead:beef:dead:beef:dead:beef")}, + } + return t +} + +func newNetworkGridTestPayload() *syntheticspb.Test { + t := newTestPayload() + t.Name = "network-grid-test" + t.Type = "network_grid" + t.Id = networkGridTestID + t.Settings.Definition = &syntheticspb.TestSettings_NetworkGrid{ + NetworkGrid: &syntheticspb.IpTest{ + Targets: []string{"192.0.2.213", "2001:db8:dead:beef:dead:beef:dead:beef"}, + }, + } + return t +} + +func newHostnameTest() *synthetics.Test { + t := newTest() + t.Name = "hostname-test" + t.Type = synthetics.TestTypeHostname + t.ID = hostnameTestID + t.Settings.Definition = &synthetics.TestDefinitionHostname{ + Target: "www.example.com", + } + return t +} + +func newHostnameTestPayload() *syntheticspb.Test { + t := newTestPayload() + t.Name = "hostname-test" + t.Type = "hostname" + t.Id = hostnameTestID + t.Settings.Definition = &syntheticspb.TestSettings_Hostname{ + Hostname: &syntheticspb.HostnameTest{ + Target: "www.example.com", + }, + } + return t +} + +func newAgentTest() *synthetics.Test { + t := newTest() + t.Name = "agent-test" + t.Type = synthetics.TestTypeAgent + t.ID = agentTestID + t.Settings.Definition = &synthetics.TestDefinitionAgent{ + Target: "dummy-agent-id", + UseLocalIP: true, + } + return t +} + +func newAgentTestPayload() *syntheticspb.Test { + t := newTestPayload() + t.Name = "agent-test" + t.Type = "agent" + t.Id = agentTestID + t.Settings.Definition = &syntheticspb.TestSettings_Agent{ + Agent: &syntheticspb.AgentTest{ + Target: "dummy-agent-id", + UseLocalIp: true, + }, + } + return t +} + +func newNetworkMeshTest() *synthetics.Test { + t := newTest() + t.Name = "network-mesh-test" + t.Type = synthetics.TestTypeNetworkMesh + t.ID = networkMeshTestID + t.Settings.Definition = &synthetics.TestDefinitionNetworkMesh{ + UseLocalIP: true, + } + return t +} + +func newNetworkMeshTestPayload() *syntheticspb.Test { + t := newTestPayload() + t.Name = "network-mesh-test" + t.Type = "network_mesh" + t.Id = networkMeshTestID + t.Settings.Definition = &syntheticspb.TestSettings_NetworkMesh{ + NetworkMesh: &syntheticspb.NetworkMeshTest{ + UseLocalIp: true, + }, + } + return t +} + +func newFlowTest() *synthetics.Test { + t := newTest() + t.Name = "flow-test" + t.Type = synthetics.TestTypeFlow + t.ID = flowTestID + t.Settings.Definition = &synthetics.TestDefinitionFlow{ + Type: synthetics.FlowTestTypeCity, + Target: "Warsaw", + TargetRefreshInterval: 168 * time.Hour, + MaxIPTargets: 10, + MaxProviders: 3, + Direction: synthetics.DirectionSrc, + InetDirection: synthetics.DirectionDst, + } + return t +} + +func newFlowTestPayload() *syntheticspb.Test { + t := newTestPayload() + t.Name = "flow-test" + t.Type = "flow" + t.Id = flowTestID + t.Settings.Definition = &syntheticspb.TestSettings_Flow{ + Flow: &syntheticspb.FlowTest{ + Target: "Warsaw", + TargetRefreshIntervalMillis: 604800000, + MaxProviders: 3, + MaxIpTargets: 10, + Type: "city", + InetDirection: "dst", + Direction: "src", + }, + } + return t +} + +func newURLTest() *synthetics.Test { + t := newTest() + t.Name = "url-test" + t.Type = synthetics.TestTypeURL + t.ID = urlTestID + t.Settings.Definition = &synthetics.TestDefinitionURL{ + Target: url.URL{ + Scheme: "https", + Host: "www.example.com:443", + RawQuery: "dummy=query", + }, + Timeout: time.Minute, + Method: http.MethodGet, + Headers: map[string]string{ + "dummy-key-1": "dummy-value-1", + "dummy-key-2": "dummy-value-2", + }, + Body: "dummy-body", + IgnoreTLSErrors: true, + } + return t +} + +func newURLTestPayload() *syntheticspb.Test { + t := newTestPayload() + t.Name = "url-test" + t.Type = "url" + t.Id = urlTestID + t.Settings.Definition = &syntheticspb.TestSettings_Url{ + Url: &syntheticspb.UrlTest{ + Target: "https://www.example.com:443?dummy=query", + Timeout: 60000, + Method: "GET", + Headers: map[string]string{ + "dummy-key-1": "dummy-value-1", + "dummy-key-2": "dummy-value-2", + }, + Body: "dummy-body", + IgnoreTlsErrors: true, + }, + } + return t +} + +func newPageLoadTest() *synthetics.Test { + t := newTest() + t.Name = "page-load-test" + t.Type = synthetics.TestTypePageLoad + t.ID = pageLoadTestID + t.Settings.Definition = &synthetics.TestDefinitionPageLoad{ + Target: url.URL{ + Scheme: "https", + Host: "www.example.com:443", + RawQuery: "dummy=query", + }, + Timeout: time.Minute, + Headers: map[string]string{ + "dummy-key-1": "dummy-value-1", + "dummy-key-2": "dummy-value-2", + }, + CSSSelectors: map[string]string{ + "dummy-key-1": "dummy-selector-1", + "dummy-key-2": "dummy-selector-2", + }, + IgnoreTLSErrors: true, + } + return t +} + +func newPageLoadTestPayload() *syntheticspb.Test { + t := newTestPayload() + t.Name = "page-load-test" + t.Type = "page_load" + t.Id = pageLoadTestID + t.Settings.Definition = &syntheticspb.TestSettings_PageLoad{ + PageLoad: &syntheticspb.PageLoadTest{ + Target: "https://www.example.com:443?dummy=query", + Timeout: 60000, + Headers: map[string]string{ + "dummy-key-1": "dummy-value-1", + "dummy-key-2": "dummy-value-2", + }, + IgnoreTlsErrors: true, + CssSelectors: map[string]string{ + "dummy-key-1": "dummy-selector-1", + "dummy-key-2": "dummy-selector-2", + }, + }, + } + return t +} + +func newDNSTest() *synthetics.Test { + t := newTest() + t.Name = "dns-test" + t.Type = synthetics.TestTypeDNS + t.ID = dnsTestID + t.Settings.Definition = &synthetics.TestDefinitionDNS{ + Target: "www.example.com", + Timeout: time.Minute, + RecordType: synthetics.DNSRecordAAAA, + Servers: []net.IP{net.ParseIP("192.0.2.213"), net.ParseIP("2001:db8:dead:beef:dead:beef:dead:beef")}, + Port: 53, + } + return t +} + +func newDNSTestPayload() *syntheticspb.Test { + t := newTestPayload() + t.Name = "dns-test" + t.Type = "dns" + t.Id = dnsTestID + t.Settings.Definition = &syntheticspb.TestSettings_Dns{ + Dns: &syntheticspb.DnsTest{ + Target: "www.example.com", + Timeout: 60000, + RecordType: syntheticspb.DNSRecord_DNS_RECORD_AAAA, + Servers: []string{"192.0.2.213", "2001:db8:dead:beef:dead:beef:dead:beef"}, + Port: 53, + }, + } + return t +} + +func newDNSGridTest() *synthetics.Test { + t := newTest() + t.Name = "dns-grid-test" + t.Type = synthetics.TestTypeDNSGrid + t.ID = dnsGridTestID + t.Settings.Definition = &synthetics.TestDefinitionDNSGrid{ + Target: "www.example.com", + Timeout: time.Minute, + RecordType: synthetics.DNSRecordAAAA, + Servers: []net.IP{net.ParseIP("192.0.2.213"), net.ParseIP("2001:db8:dead:beef:dead:beef:dead:beef")}, + Port: 53, + } + return t +} + +func newDNSGridTestPayload() *syntheticspb.Test { + t := newTestPayload() + t.Name = "dns-grid-test" + t.Type = "dns_grid" + t.Id = dnsGridTestID + t.Settings.Definition = &syntheticspb.TestSettings_DnsGrid{ + DnsGrid: &syntheticspb.DnsTest{ + Target: "www.example.com", + Timeout: 60000, + RecordType: syntheticspb.DNSRecord_DNS_RECORD_AAAA, + Servers: []string{"192.0.2.213", "2001:db8:dead:beef:dead:beef:dead:beef"}, + Port: 53, + }, + } + return t +} + +func newBGPMonitorTest() *synthetics.Test { + t := newTest() + t.Name = "bgp-monitor-test" + t.Type = "bgp_monitor" + t.ID = bgpMonitorTestID + return t +} + +func newBGPMonitorTestPayload() *syntheticspb.Test { + t := newTestPayload() + t.Name = "bgp-monitor-test" + t.Type = "bgp_monitor" + t.Id = bgpMonitorTestID + return t +} + +func newTransactionTest() *synthetics.Test { + t := newTest() + t.Name = "transaction-test" + t.Type = "transaction" + t.ID = transactionTestID + return t +} + +func newTransactionTestPayload() *syntheticspb.Test { + t := newTestPayload() + t.Name = "transaction-test" + t.Type = "transaction" + t.Id = transactionTestID + return t +} + +func newTest() *synthetics.Test { + return &synthetics.Test{ + Name: "dummy-test", + Type: "unknown-type", + Status: synthetics.TestStatusActive, + UpdateDate: pointer.ToTime(time.Date(2022, time.April, 8, 7, 26, 51, 505*1000000, time.UTC)), + Settings: synthetics.TestSettings{ + Definition: nil, + AgentIDs: []string{"817", "818", "819"}, + Period: 60 * time.Second, + Family: synthetics.IPFamilyDual, + NotificationChannels: []string{"7143fa58", "14f23014"}, + Health: synthetics.HealthSettings{ + LatencyCritical: 1 * time.Millisecond, + LatencyWarning: 2 * time.Millisecond, + LatencyCriticalStdDev: 3 * time.Millisecond, + LatencyWarningStdDev: 4 * time.Millisecond, + JitterCritical: 5 * time.Millisecond, + JitterWarning: 6 * time.Millisecond, + JitterCriticalStdDev: 7 * time.Millisecond, + JitterWarningStdDev: 8 * time.Millisecond, + PacketLossCritical: 9, + PacketLossWarning: 10, + HTTPLatencyCritical: 11 * time.Millisecond, + HTTPLatencyWarning: 12 * time.Millisecond, + HTTPLatencyCriticalStdDev: 13 * time.Millisecond, + HTTPLatencyWarningStdDev: 14 * time.Millisecond, + HTTPValidCodes: []uint32{http.StatusOK, http.StatusCreated}, + DNSValidCodes: []uint32{1, 2, 3}, + UnhealthySubtestThreshold: 42, + AlarmActivation: &synthetics.AlarmActivationSettings{ + TimeWindow: 5 * time.Minute, + Times: 3, + GracePeriod: 2, + }, + }, + Ping: &synthetics.PingSettings{ + Timeout: 3 * time.Second, + Count: 5, + Delay: 1 * time.Millisecond, + Protocol: synthetics.PingProtocolTCP, + Port: 443, + }, + Traceroute: &synthetics.TracerouteSettings{ + Timeout: 22500 * time.Millisecond, + Count: 3, + Delay: 1 * time.Millisecond, + Protocol: synthetics.TracerouteProtocolUDP, + Port: 33434, + Limit: 30, + }, + Tasks: []synthetics.TaskType{synthetics.TaskTypePing, synthetics.TaskTypeTraceroute}, + }, + ID: "dummy-id", + CreateDate: time.Date(2022, time.April, 6, 9, 43, 39, 324*1000000, time.UTC), + CreatedBy: synthetics.UserInfo{ + ID: "4321", + Email: "joe.doe@example.com", + FullName: "Joe Doe", + }, + LastUpdatedBy: &synthetics.UserInfo{ + ID: "4321", + Email: "joe.doe@example.com", + FullName: "Joe Doe", + }, + } +} + +func newTestPayload() *syntheticspb.Test { + return &syntheticspb.Test{ + Id: "dummy-id", + Name: "dummy-test", + Type: "dummy-type", + Status: syntheticspb.TestStatus_TEST_STATUS_ACTIVE, + Settings: &syntheticspb.TestSettings{ + Definition: nil, + AgentIds: []string{"817", "818", "819"}, + Tasks: []string{"ping", "traceroute"}, + HealthSettings: &syntheticspb.HealthSettings{ + LatencyCritical: 1, + LatencyWarning: 2, + PacketLossCritical: 9, + PacketLossWarning: 10, + JitterCritical: 5, + JitterWarning: 6, + HttpLatencyCritical: 11, + HttpLatencyWarning: 12, + HttpValidCodes: []uint32{200, 201}, + DnsValidCodes: []uint32{1, 2, 3}, + LatencyCriticalStddev: 3, + LatencyWarningStddev: 4, + JitterCriticalStddev: 7, + JitterWarningStddev: 8, + HttpLatencyCriticalStddev: 13, + HttpLatencyWarningStddev: 14, + UnhealthySubtestThreshold: 42, + Activation: &syntheticspb.ActivationSettings{ + GracePeriod: "2", + TimeUnit: "m", + TimeWindow: "5", + Times: "3", + }, + }, + Ping: &syntheticspb.TestPingSettings{ + Count: 5, + Protocol: "tcp", + Port: 443, + Timeout: 3000, + Delay: 1, + }, + Trace: &syntheticspb.TestTraceSettings{ + Count: 3, + Protocol: "udp", + Port: 33434, + Timeout: 22500, + Limit: 30, + Delay: 1, + }, + Period: 60, + Family: syntheticspb.IPFamily_IP_FAMILY_DUAL, + NotificationChannels: []string{"7143fa58", "14f23014"}, + }, + Cdate: timestamppb.New(time.Date(2022, time.April, 6, 9, 43, 39, 324*1000000, time.UTC)), + Edate: timestamppb.New(time.Date(2022, time.April, 8, 7, 26, 51, 505*1000000, time.UTC)), + CreatedBy: &syntheticspb.UserInfo{ + Id: "4321", + Email: "joe.doe@example.com", + FullName: "Joe Doe", + }, + LastUpdatedBy: &syntheticspb.UserInfo{ + Id: "4321", + Email: "joe.doe@example.com", + FullName: "Joe Doe", + }, + } +} diff --git a/kentikapi/synthetics/agent.go b/kentikapi/synthetics/agent.go index 281c925..7366479 100644 --- a/kentikapi/synthetics/agent.go +++ b/kentikapi/synthetics/agent.go @@ -38,7 +38,7 @@ type Agent struct { // CloudProvider is the name of the cloud provider for agents hosted in a public cloud (otherwise an empty string). CloudProvider cloud.Provider // CloudRegion is the name of the cloud region for agents hosted in a public cloud (otherwise an empty string). - // Allowed values: valid name of a region for the cloud provider. + // Allowed values: a valid name of a region for the cloud provider. CloudRegion string // Read-only properties diff --git a/kentikapi/synthetics/test.go b/kentikapi/synthetics/test.go new file mode 100644 index 0000000..4b39158 --- /dev/null +++ b/kentikapi/synthetics/test.go @@ -0,0 +1,760 @@ +package synthetics + +import ( + "net" + "net/url" + "time" + + "github.com/kentik/community_sdk_golang/kentikapi/models" +) + +// NewIPTest creates a new IP test with all required fields set. +func NewIPTest(obj IPTestRequiredFields) *Test { + t := newBasePingTraceTest(obj.BasePingTraceTestRequiredFields) + t.Type = TestTypeIP + t.Settings.Definition = &obj.Definition + t.Settings.Tasks = []TaskType{TaskTypePing, TaskTypeTraceroute} + return t +} + +// NewNetworkGridTest creates a new NetworkGrid test with all required fields set. +func NewNetworkGridTest(obj NetworkGridTestRequiredFields) *Test { + t := newBasePingTraceTest(obj.BasePingTraceTestRequiredFields) + t.Type = TestTypeNetworkGrid + t.Settings.Definition = &obj.Definition + t.Settings.Tasks = []TaskType{TaskTypePing, TaskTypeTraceroute} + return t +} + +// NewHostnameTest creates a new hostname test with all required fields set. +func NewHostnameTest(obj HostnameTestRequiredFields) *Test { + t := newBasePingTraceTest(obj.BasePingTraceTestRequiredFields) + t.Type = TestTypeHostname + t.Settings.Definition = &obj.Definition + t.Settings.Tasks = []TaskType{TaskTypePing, TaskTypeTraceroute} + return t +} + +// NewAgentTest creates a new agent test with all required fields set. +func NewAgentTest(obj AgentTestRequiredFields) *Test { + t := newBasePingTraceTest(obj.BasePingTraceTestRequiredFields) + t.Type = TestTypeAgent + t.Settings.Definition = &TestDefinitionAgent{ + Target: obj.Definition.Target, + } + t.Settings.Tasks = []TaskType{TaskTypePing, TaskTypeTraceroute} + return t +} + +// NewNetworkMeshTest creates a new network mesh test with all required fields set. +func NewNetworkMeshTest(obj NetworkMeshTestRequiredFields) *Test { + t := newBasePingTraceTest(obj.BasePingTraceTestRequiredFields) + t.Type = TestTypeNetworkMesh + t.Settings.Definition = &TestDefinitionNetworkMesh{} + t.Settings.Tasks = []TaskType{TaskTypePing, TaskTypeTraceroute} + return t +} + +// NewFlowTest creates a new flow test with all required fields set. +func NewFlowTest(obj FlowTestRequiredFields) *Test { + t := newBasePingTraceTest(obj.BasePingTraceTestRequiredFields) + t.Type = TestTypeFlow + t.Settings.Definition = &TestDefinitionFlow{ + Type: obj.Definition.Type, + Target: obj.Definition.Target, + Direction: obj.Definition.Direction, + InetDirection: obj.Definition.InetDirection, + } + t.Settings.Tasks = []TaskType{TaskTypePing, TaskTypeTraceroute} + return t +} + +// NewURLTest creates a new URL test with all required fields set. +func NewURLTest(obj URLTestRequiredFields) *Test { + t := newBaseTest(obj.BaseTestRequiredFields) + t.Type = TestTypeURL + t.Settings.Definition = &TestDefinitionURL{ + Target: obj.Definition.Target, + Timeout: obj.Definition.Timeout, + } + t.Settings.Tasks = []TaskType{TaskTypeHTTP} // ping and traceroute tasks are optional + return t +} + +// NewPageLoadTest creates a new page load test with all required fields set. +func NewPageLoadTest(obj PageLoadTestRequiredFields) *Test { + t := newBaseTest(obj.BaseTestRequiredFields) + t.Type = TestTypePageLoad + t.Settings.Definition = &TestDefinitionPageLoad{ + Target: obj.Definition.Target, + Timeout: obj.Definition.Timeout, + } + t.Settings.Tasks = []TaskType{TaskTypePageLoad} // ping and traceroute tasks are optional + return t +} + +// NewDNSTest creates a new DNS test with all required fields set. +func NewDNSTest(obj DNSTestRequiredFields) *Test { + t := newBaseTest(obj.BaseTestRequiredFields) + t.Type = TestTypeDNS + t.Settings.Definition = &obj.Definition + t.Settings.Tasks = []TaskType{TaskTypeDNS} + return t +} + +// NewDNSGridTest creates a new DNS grid test with all required fields set. +func NewDNSGridTest(obj DNSGridTestRequiredFields) *Test { + t := newBaseTest(obj.BaseTestRequiredFields) + t.Type = TestTypeDNSGrid + t.Settings.Definition = &obj.Definition + t.Settings.Tasks = []TaskType{TaskTypeDNS} + return t +} + +func newBasePingTraceTest(obj BasePingTraceTestRequiredFields) *Test { + t := newBaseTest(obj.BaseTestRequiredFields) + t.Settings.Ping = &PingSettings{ + Timeout: obj.Ping.Timeout, + Count: obj.Ping.Count, + Protocol: obj.Ping.Protocol, + Port: obj.Ping.Port, + } + t.Settings.Traceroute = &TracerouteSettings{ + Timeout: obj.Traceroute.Timeout, + Count: obj.Traceroute.Count, + Delay: obj.Traceroute.Delay, + Protocol: obj.Traceroute.Protocol, + Limit: obj.Traceroute.Limit, + } + return t +} + +func newBaseTest(obj BaseTestRequiredFields) *Test { + return &Test{ + Name: obj.Name, + Settings: TestSettings{ + AgentIDs: obj.AgentIDs, + }, + } +} + +// GetAllTestsResponse model. +type GetAllTestsResponse struct { + // Tests holds all tests. + Tests []Test + // InvalidTestsCount is a number of invalid tests. + InvalidTestsCount uint32 +} + +// Test is synthetics test model. +type Test struct { + // Read-write properties + + // Name is user selected name for the test. + Name string + // Type is the specified type of the test. It must be provided on test creation and becomes read-only after that. + Type TestType + // Status is the life-cycle status of the test. + Status TestStatus + // UpdateDate is the last modification timestamp. If provided in update request, the API returns error + // if the object to be modified has been modified in the DB after the UpdateDate timestamp. This allows + // to guard against concurrent modifications. + UpdateDate *time.Time + + // Read-only properties + + // Settings contains test configuration attributes. + Settings TestSettings + // ID is unique test identification. It is read-only. + ID models.ID + // CreateDate is the creation timestamp. It is read-only. + CreateDate time.Time + // CreatedBy is an identity of the user that has created the test. It is read-only. + CreatedBy UserInfo + // LastUpdatedBy is the identity of the user that has modified the test last. It is read-only. + LastUpdatedBy *UserInfo +} + +// TestSettings contains test configuration attributes. +type TestSettings struct { + // Definition contains test type specific configuration attributes. + Definition TestDefinition + // AgentIDs contains IDs of agents that shall execute tasks for this test. Only existing agents in the account + // are allowed. + AgentIDs []models.ID + // Period is a test execution period. Default: 60s. Allowed values range: [1 s, 900 s]. + Period time.Duration + // Family selects which type of DNS resource is queried for resolving hostname to target address. + // It is used only for DNS and HTTP class of tests. Default: IPFamilyDual. + Family IPFamily + // NotificationChannels is a list of notifications channels for the tests. It must contain IDs of existing + // notification channels. + NotificationChannels []string + // Health is a configuration of thresholds, acceptable status codes for evaluating test health + // and activation conditions for alarms. + Health HealthSettings + // Ping is a configuration of ping task for the test. + Ping *PingSettings + // Traceroute if a configuration of traceroute task for the test. + Traceroute *TracerouteSettings + // Tasks is a list of names of tasks that shall be executed on behalf of this test. + // Valid combinations of tasks depend on test type: + // - IP, network grid, hostname, agent, network mesh and flow test types - ping and traceroute tasks (required) + // - URL test type - HTTP task (required); ping and traceroute tasks (optional) + // - page load test type - page load task (required); ping and traceroute tasks (optional) + // - DNS and DNS grid test types - DNS task (required) + // The system supports only running both ping and traceroute tasks together (as opposed to ping or traceroute task + // individually). + Tasks []TaskType +} + +func (s TestSettings) GetIPDefinition() *TestDefinitionIP { + d, _ := s.Definition.(*TestDefinitionIP) //nolint:errcheck // user can check the pointer + return d +} + +func (s TestSettings) GetNetworkGridDefinition() *TestDefinitionNetworkGrid { + d, _ := s.Definition.(*TestDefinitionNetworkGrid) //nolint:errcheck // user can check the pointer + return d +} + +func (s TestSettings) GetHostnameDefinition() *TestDefinitionHostname { + d, _ := s.Definition.(*TestDefinitionHostname) //nolint:errcheck // user can check the pointer + return d +} + +func (s TestSettings) GetAgentDefinition() *TestDefinitionAgent { + d, _ := s.Definition.(*TestDefinitionAgent) //nolint:errcheck // user can check the pointer + return d +} + +func (s TestSettings) GetNetworkMeshDefinition() *TestDefinitionNetworkMesh { + d, _ := s.Definition.(*TestDefinitionNetworkMesh) //nolint:errcheck // user can check the pointer + return d +} + +func (s TestSettings) GetFlowDefinition() *TestDefinitionFlow { + d, _ := s.Definition.(*TestDefinitionFlow) //nolint:errcheck // user can check the pointer + return d +} + +func (s TestSettings) GetURLDefinition() *TestDefinitionURL { + d, _ := s.Definition.(*TestDefinitionURL) //nolint:errcheck // user can check the pointer + return d +} + +func (s TestSettings) GetPageLoadDefinition() *TestDefinitionPageLoad { + d, _ := s.Definition.(*TestDefinitionPageLoad) //nolint:errcheck // user can check the pointer + return d +} + +func (s TestSettings) GetDNSDefinition() *TestDefinitionDNS { + d, _ := s.Definition.(*TestDefinitionDNS) //nolint:errcheck // user can check the pointer + return d +} + +func (s TestSettings) GetDNSGridDefinition() *TestDefinitionDNSGrid { + d, _ := s.Definition.(*TestDefinitionDNSGrid) //nolint:errcheck // user can check the pointer + return d +} + +// TestDefinition emulates a union of: +// - TestDefinitionIP +// - TestDefinitionNetworkGrid +// - TestDefinitionHostname +// - TestDefinitionAgent +// - TestDefinitionNetworkMesh +// - TestDefinitionFlow +// - TestDefinitionURL +// - TestDefinitionPageLoad +// - TestDefinitionDNS +// - TestDefinitionDNSGrid +// Note that the interface is implemented with pointer receiver, so that definition's fields can be updated easily. +type TestDefinition interface { + isTestDefinition() +} + +// TestDefinitionIP contains the definition of TestTypeIP test. +type TestDefinitionIP struct { + // Targets define target IP addresses. + Targets []net.IP +} + +func (d *TestDefinitionIP) isTestDefinition() {} + +// TestDefinitionNetworkGrid contains the definition of TestTypeNetworkGrid test. +type TestDefinitionNetworkGrid struct { + // Targets define target IP addresses. + Targets []net.IP +} + +func (d *TestDefinitionNetworkGrid) isTestDefinition() {} + +// TestDefinitionHostname contains the definition of TestTypeHostname test. +type TestDefinitionHostname struct { + // Target defines target fully qualified DNS name. + Target string +} + +func (d *TestDefinitionHostname) isTestDefinition() {} + +// TestDefinitionAgent contains the definition of TestTypeAgent test. +type TestDefinitionAgent struct { + // Target is an ID of target agent. Valid ID of an existing agent must be provided. + Target models.ID + // UseLocalIP indicates whether to use the configured "local" (internal) IP address as the source + // when initiating probes. + UseLocalIP bool +} + +func (d *TestDefinitionAgent) isTestDefinition() {} + +// TestDefinitionNetworkMesh contains the definition of TestTypeNetworkMesh test. +type TestDefinitionNetworkMesh struct { + // UseLocalIP indicates whether to use the configured "local" (internal) IP address as the source + // when initiating probes. + UseLocalIP bool +} + +func (d *TestDefinitionNetworkMesh) isTestDefinition() {} + +// TestDefinitionFlow contains the definition of TestTypeFlow test. +type TestDefinitionFlow struct { + // Type defines subtype of the test, which also specifies the type of target. + Type FlowTestType + // Target is a value to be matched in the query. Depending on Type, it must be: AS number, CDN name, country name, + // region name or city name. + Target string + // TargetRefreshInterval is a period between regenerating list of targets based on flow data query. + // Default: 0. Allowed values: 0 or range [1 hour, 168 hours]. + TargetRefreshInterval time.Duration + // MaxIPTargets is maximum number of target IP addresses to select based on flow data query. + // Default: 10. Allowed values range: [1, 20]. + MaxIPTargets uint32 + // MaxProviders is a maximum number of providers tracked for selection of target IP addresses. + // Default: 3. Allowed values range: [1, 10]. + MaxProviders uint32 + // Direction specifies whether to match the (sub) type attribute in source or destination of flows + // in the flow data query. + Direction Direction + // InetDirection specifies whether to use source address in inbound flows or destination addresses + // in outbound flows. + InetDirection Direction +} + +func (d *TestDefinitionFlow) isTestDefinition() {} + +// TestDefinitionURL contains the definition of TestTypeURL test. +type TestDefinitionURL struct { + // Target is a URL to use in the HTTP request. + Target url.URL + // Timeout is an HTTP request timeout. Allowed values range: [5 s, 60 s]. + Timeout time.Duration + // Method is an HTTP method to use in the request. Default: GET. Allowed values: GET, PATCH, POST, PUT. + Method string + // Headers is a set of key-value pairs to be included among HTTP headers in the request. Valid HTTP header names + // and values are expected. + Headers map[string]string + // Body is a content to be placed in the body of the request. + Body string + // IgnoreTLSErrors is an indication whether to ignore errors reported in TLS session establishment. + IgnoreTLSErrors bool +} + +func (d *TestDefinitionURL) isTestDefinition() {} + +// TestDefinitionPageLoad contains the definition of TestTypePageLoad test. +type TestDefinitionPageLoad struct { + // Target is a URL to use in the HTTP request. + Target url.URL + // Timeout is an HTTP request timeout. Allowed values range: [5 s, 60 s]. + Timeout time.Duration + // Headers is a set of key-value pairs to be included among HTTP headers in the request. Valid HTTP header names + // and values are expected. + Headers map[string]string + // CSSSelectors is a set of key-value pairs to set as CSS selectors in the request. Valid HTTP CSS selector keys + // and values are expected. + CSSSelectors map[string]string + // IgnoreTLSErrors is an indication whether to ignore errors reported in TLS session establishment. + IgnoreTLSErrors bool +} + +func (d *TestDefinitionPageLoad) isTestDefinition() {} + +// TestDefinitionDNS contains the definition of TestTypeDNS test. +type TestDefinitionDNS struct { + // Target is a fully qualified DNS name to resolve. + Target string + // Timeout is a DNS request timeout. Allowed values range: [5 s, 60 s]. + Timeout time.Duration + // RecordType is a type of DNS record to query. + RecordType DNSRecord + // Servers is a list of addresses of servers to query. At least one entry is required. + Servers []net.IP + // Port is a server port to use. Allowed values range: [1, 65535]. + Port uint32 +} + +func (d *TestDefinitionDNS) isTestDefinition() {} + +// TestDefinitionDNSGrid contains the definition of TestTypeDNSGrid test. +type TestDefinitionDNSGrid = TestDefinitionDNS + +// HealthSettings is a configuration of thresholds, acceptable status codes for evaluating test health +// and activation conditions for alarms. +type HealthSettings struct { + // LatencyCritical is a threshold for critical level of the average value of latency. + // 0 means no health check. Default: 0. Allowed values: >= 0. + LatencyCritical time.Duration + // LatencyWarning is a threshold for warning level of the average value of latency. + // 0 means no health check. Default: 0. Allowed values: >= 0. + LatencyWarning time.Duration + // LatencyCriticalStdDev is a threshold for critical level of the standard deviation of latency. + // 0 means no health check. Default: 0. Allowed values range: [0, 100 ms]. + LatencyCriticalStdDev time.Duration + // LatencyWarningStdDev is a threshold for warning level of the standard deviation of latency. + // 0 means no health check. Default: 0. Allowed values range: [0, 100 ms]. + LatencyWarningStdDev time.Duration + // JitterCritical is a threshold for critical level of the average value of jitter. + // 0 means no health check. Default: 0. Allowed values: >= 0. + JitterCritical time.Duration + // JitterWarning is a threshold for warning level of the average value of jitter. + // 0 means no health check. Default: 0. Allowed values: >= 0. + JitterWarning time.Duration + // JitterCriticalStdDev is a threshold for critical level of the standard deviation of jitter. + // 0 means no health check. Default: 0. Allowed values range: [0, 100 ms]. + JitterCriticalStdDev time.Duration + // JitterWarningStdDev is a threshold for warning level of the standard deviation of jitter. + // 0 means no health check. Default: 0. Allowed values range: [0, 100 ms]. + JitterWarningStdDev time.Duration + // PacketLossCritical is a threshold for critical level of packet loss (in percents). + // 0 means no health check. Default: 0. Allowed values range: [0, 100]. + PacketLossCritical float32 + // PacketLossWarning is a threshold for warning level of packet loss (in percents). + // 0 means no health check. Default: 0. Allowed values range: [0, 100]. + PacketLossWarning float32 + // HTTPLatencyCritical is a threshold for critical level of the average value of HTTP response latency. + // 0 means no health check. Default: 0. Allowed values: >= 0. + HTTPLatencyCritical time.Duration + // HTTPLatencyWarning is a threshold for warning level of the average value of HTTP response latency. + // 0 means no health check. Default: 0. Allowed values: >= 0. + HTTPLatencyWarning time.Duration + // HTTPLatencyCriticalStdDev is a threshold for critical level of the standard deviation of HTTP response latency. + // 0 means no health check. Default: 0. Allowed values range: [0, 100 ms]. + HTTPLatencyCriticalStdDev time.Duration + // HTTPLatencyWarningStdDev is a threshold for warning level of the standard deviation of HTTP response latency + // 0 means no health check. Default: 0. Allowed values range: [0, 100 ms]. + HTTPLatencyWarningStdDev time.Duration + // HTTPValidCodes is a list of HTTP result codes indicating success. Only valid HTTP result codes are accepted. + // Empty list means result code is not checked. + HTTPValidCodes []uint32 + // DNSValidCodes is a list of DNS result codes indicating success. Only valid DNS result codes are accepted. + // Empty list means result code is not checked. + DNSValidCodes []uint32 + // UnhealthySubtestThreshold is a number of tasks that has to be declared unhealthy in order for the test + // to be declared unhealthy. Default: 1. Allowed values: > 0. + UnhealthySubtestThreshold uint32 + // AlarmActivation sets activation conditions for alarms generated based on health thresholds. + AlarmActivation *AlarmActivationSettings +} + +// AlarmActivationSettings sets activation conditions for alarms generated based on health thresholds. +type AlarmActivationSettings struct { + // TimeWindow is an activation window. The value in seconds must be greater or equal + // to TestSettings.Period * (times + 1). Default: 5 minutes. + TimeWindow time.Duration + // Times is a minimum number of unhealthy test events within the TimeWindow for alarm activation. + // Default: 3. Allowed values: > 0. + Times uint + // Grace period is a maximum duration of continuous test healthy state not canceling test activation. Default: 2. + GracePeriod uint +} + +// PingSettings is a configuration of ping task for the test. +type PingSettings struct { + // Timeout is a total timeout for one execution of the task. Allowed values range: [1 ms, 10000 ms]. + Timeout time.Duration + // Count is a number of probe packets per one task execution. Allowed values: [1, 10]. + Count uint32 + // Delay is a delay between sending of individual probe packets. Default: 0. Allowed values: >= 0. + Delay time.Duration + // Protocol is a type of probe packets. Ping task sends either ICMP echo request or performs TCP half-connect + // to specified destination port (SYN, SYN-ACK, RST). + Protocol PingProtocol + // Port is a destination TCP port to use in probe packets. It is required for TCP protocol. + // Allowed values range: [1, 65535] for TCP; 0 for ICMP. + Port uint32 +} + +// TracerouteSettings if a configuration of traceroute task for the test. +type TracerouteSettings struct { + // Timeout is a total timeout for one execution of the task. + // Allowed values range: [1 ms, 5 m]. It must be lower than TestSettings.Period. + Timeout time.Duration + // Count is a number of probe packets per one patch hop. Allowed values: [1, 5]. + Count uint32 + // Delay is a delay between sending of individual probe packets. Allowed values: >= 0. + Delay time.Duration + // Protocol is a type of probe packets. + Protocol TracerouteProtocol + // Port is a destination TCP or UDP port to use in probe packets. Default: 33434. + // Allowed values range: [1, 65535] for TCP or UDP; 0 for ICMP. + Port uint32 + // Limit is maximum number of hops to probe (e.e. maximum TTL). Allowed values range: [1, 255]. + Limit uint32 +} + +// UserInfo contains user identity information. +type UserInfo struct { + // ID is unique identification of the user. It is read-only. + ID models.ID + // Email is e-mail address of the user. It is read-only. + Email string + // FullName is full name of the user. It is read-only. + FullName string +} + +// IPTestRequiredFields is a subset of Test fields required to create an IP test. +type IPTestRequiredFields struct { + BasePingTraceTestRequiredFields + Definition TestDefinitionIPRequiredFields +} + +// TestDefinitionIPRequiredFields is a subset of TestDefinitionIP fields required to create an IP test. +// Currently, it contains all TestDefinitionIP fields. +type TestDefinitionIPRequiredFields = TestDefinitionIP + +// NetworkGridTestRequiredFields is a subset of Test fields required to create a NetworkGrid test. +type NetworkGridTestRequiredFields struct { + BasePingTraceTestRequiredFields + Definition TestDefinitionNetworkGridRequiredFields +} + +// TestDefinitionNetworkGridRequiredFields is a subset of TestDefinitionNetworkGrid fields required to create +// a NetworkGrid test. Currently, it contains all TestDefinitionNetworkGrid fields. +type TestDefinitionNetworkGridRequiredFields = TestDefinitionNetworkGrid + +// HostnameTestRequiredFields is a subset Test of fields required to create a hostname test. +type HostnameTestRequiredFields struct { + BasePingTraceTestRequiredFields + Definition TestDefinitionHostnameRequiredFields +} + +// TestDefinitionHostnameRequiredFields is a subset of TestDefinitionHostname fields required to create a hostname test. +// Currently, it contains all TestDefinitionHostname fields. +type TestDefinitionHostnameRequiredFields = TestDefinitionHostname + +// AgentTestRequiredFields is a subset Test of fields required to create an agent test. +type AgentTestRequiredFields struct { + BasePingTraceTestRequiredFields + Definition TestDefinitionAgentRequiredFields +} + +// TestDefinitionAgentRequiredFields is a subset of TestDefinitionAgent fields required to create an agent test. +type TestDefinitionAgentRequiredFields struct { + Target models.ID +} + +// NetworkMeshTestRequiredFields is a subset Test of fields required to create a network mesh test. +type NetworkMeshTestRequiredFields struct { + BasePingTraceTestRequiredFields + // Definition contains no required fields +} + +// FlowTestRequiredFields is a subset Test of fields required to create a flow test. +type FlowTestRequiredFields struct { + BasePingTraceTestRequiredFields + Definition TestDefinitionFlowRequiredFields +} + +// TestDefinitionFlowRequiredFields is a subset of TestDefinitionFlow fields required to create a flow test. +type TestDefinitionFlowRequiredFields struct { + Type FlowTestType + Target string + Direction Direction + InetDirection Direction +} + +// URLTestRequiredFields is a subset Test of fields required to create a URL test. +type URLTestRequiredFields struct { + BaseTestRequiredFields + Definition TestDefinitionURLRequiredFields +} + +// TestDefinitionURLRequiredFields is a subset of TestDefinitionURL fields required to create a URL test. +type TestDefinitionURLRequiredFields struct { + Target url.URL + Timeout time.Duration +} + +// PageLoadTestRequiredFields is a subset Test of fields required to create a page load test. +type PageLoadTestRequiredFields struct { + BaseTestRequiredFields + Definition TestDefinitionPageLoadRequiredFields +} + +// TestDefinitionPageLoadRequiredFields is a subset of TestDefinitionPageLoad fields required to create +// a page load test. +type TestDefinitionPageLoadRequiredFields struct { + Target url.URL + Timeout time.Duration +} + +// DNSTestRequiredFields is a subset Test of fields required to create a DNS test. +type DNSTestRequiredFields struct { + BaseTestRequiredFields + Definition TestDefinitionDNSRequiredFields +} + +// TestDefinitionDNSRequiredFields is a subset of TestDefinitionDNS fields required to create a DNS test. +// Currently, it contains all TestDefinition fields. +type TestDefinitionDNSRequiredFields = TestDefinitionDNS + +// DNSGridTestRequiredFields is a subset Test of fields required to create a DNS grid test. +type DNSGridTestRequiredFields struct { + BaseTestRequiredFields + Definition TestDefinitionDNSGridRequiredFields +} + +// TestDefinitionDNSGridRequiredFields is a subset of TestDefinitionDNSGrid fields required to create a DNS grid test. +// Currently, it contains all TestDefinition fields. +type TestDefinitionDNSGridRequiredFields = TestDefinitionDNSGrid + +// BasePingTraceTestRequiredFields is a subset of Test fields required to create a test with ping and traceroute tasks. +type BasePingTraceTestRequiredFields struct { + BaseTestRequiredFields + Ping PingSettingsRequiredFields + Traceroute TracerouteSettingsRequiredFields +} + +// BaseTestRequiredFields is a subset of Test fields required to create any test. +type BaseTestRequiredFields struct { + Name string + AgentIDs []models.ID +} + +// PingSettingsRequiredFields is a subset of PingSettings fields required for create. +type PingSettingsRequiredFields struct { + Timeout time.Duration + Count uint32 + Protocol PingProtocol + Port uint32 +} + +// TracerouteSettingsRequiredFields is a subset of TracerouteSettings fields required for create. +type TracerouteSettingsRequiredFields struct { + Timeout time.Duration + Count uint32 + Delay time.Duration + Protocol TracerouteProtocol + Limit uint32 +} + +// TestType is the specified type of the test. +type TestType string + +const ( + // TestTypeIP allows testing of a multiple target addresses from one or more agents. + TestTypeIP TestType = "ip" + // TestTypeNetworkGrid allows testing of a multiple target addresses from one or more agents. + // It differs from the TestTypeIP only in presentation of results in the UI. + TestTypeNetworkGrid TestType = "network_grid" + // TestTypeHostname allows testing of a single target defined by DNS name. It resolves the + // DNS name to IP address and selects destination address(es) based on the family setting. If + // the target name has both IPv4 and IPv6 resolution and the family is IP_FAMILY_DUAL, ping + // and trace are run against both. + TestTypeHostname TestType = "hostname" + // TestTypeAgent allows probing from one or more agents to another agent. + TestTypeAgent TestType = "agent" + // TestTypeNetworkMesh allows to probe paths between a set of agents. Every agent probes + // every other agent.The common setting agentIds attribute is used as a list of targets for the test. + TestTypeNetworkMesh TestType = "network_mesh" + // TestTypeFlow (called “Autonomous Tests” in the UI) allow to automatically select test targets + // based on a query to flow data. The test configuration specifies parameters for the query. + TestTypeFlow TestType = "flow" + // TestTypeURL is an HTTP application test that verifies ability to execute HTTP request against + // specific URL end-point and collect observations on latency in various stages of processing + // (DNS resolution, TCP connection establishment, response latency). In addition to that it allows + // to run ping and trace tasks targeting addresses to which the hostname in the URL resolves. + TestTypeURL TestType = "url" + // TestTypePageLoad is similar to TestTypeURL but provides more detailed information + // about requests processing. + TestTypePageLoad TestType = "page_load" + // TestTypeDNS allows to test availability and latency of resolutions for a set of target DNS names + // using a set of DNS servers. There is no functional difference between this and TestTypeDNSGrid + // test, other than visual representation of results in the UI. + TestTypeDNS TestType = "dns" + // TestTypeDNSGrid is similar to TestTypeDNS described above. + TestTypeDNSGrid TestType = "dns_grid" +) + +// FlowTestType is a subtype of a synthetics flow test. +type FlowTestType string + +const ( + FlowTestTypeASN FlowTestType = "asn" + FlowTestTypeCDN FlowTestType = "cdn" + FlowTestTypeCountry FlowTestType = "country" + FlowTestTypeRegion FlowTestType = "region" + FlowTestTypeCity FlowTestType = "city" +) + +// TestStatus is the life-cycle status of the test. +type TestStatus string + +const ( + TestStatusActive TestStatus = "TEST_STATUS_ACTIVE" + TestStatusPaused TestStatus = "TEST_STATUS_PAUSED" + TestStatusDeleted TestStatus = "TEST_STATUS_DELETED" +) + +// TaskType is a name of the task that shall be executed on behalf of synthetics test. +type TaskType string + +const ( + TaskTypeDNS TaskType = "dns" + TaskTypeHTTP TaskType = "http" + TaskTypePageLoad TaskType = "page-load" + TaskTypePing TaskType = "ping" + TaskTypeTraceroute TaskType = "traceroute" +) + +// Direction is a source and destination enumeration. +type Direction string + +const ( + DirectionSrc Direction = "src" + DirectionDst Direction = "dst" +) + +// DNSRecord is a DNS record type. +type DNSRecord string + +const ( + DNSRecordUnspecified DNSRecord = "DNS_RECORD_UNSPECIFIED" + DNSRecordA DNSRecord = "DNS_RECORD_A" + DNSRecordAAAA DNSRecord = "DNS_RECORD_AAAA" + DNSRecordCName DNSRecord = "DNS_RECORD_CNAME" + DNSRecordDName DNSRecord = "DNS_RECORD_DNAME" + DNSRecordNS DNSRecord = "DNS_RECORD_NS" + DNSRecordMX DNSRecord = "DNS_RECORD_MX" + DNSRecordPTR DNSRecord = "DNS_RECORD_PTR" + DNSRecordSOA DNSRecord = "DNS_RECORD_SOA" +) + +// PingProtocol is a type of probe packets for ping task. +type PingProtocol string + +const ( + PingProtocolICMP PingProtocol = "icmp" + PingProtocolTCP PingProtocol = "tcp" +) + +// TracerouteProtocol is a type of probe packets for traceroute task. +type TracerouteProtocol string + +const ( + TracerouteProtocolICMP TracerouteProtocol = "icmp" + TracerouteProtocolTCP TracerouteProtocol = "tcp" + TracerouteProtocolUDP TracerouteProtocol = "udp" +)