Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add all possible endpoint to diagnose command #24377

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
5573522
feat(endpoint_info.go): try to add endpoint with no success
louis-cqrl Mar 26, 2024
382d9ad
Merge branch 'main' into louis-cqrl/add-all-endpoints-connectivity-di…
louis-cqrl Apr 4, 2024
20de192
remove endpoint that fail during diagnose command
louis-cqrl Apr 4, 2024
911cc34
Update working endpoints in endpoints.go
louis-cqrl Apr 5, 2024
89adcc7
Merge branch 'main' into louis-cqrl/add-all-endpoints-connectivity-di…
louis-cqrl Apr 10, 2024
06fc382
Update endpoints in defaultforwarder/endpoints.go and add connectivit…
louis-cqrl Apr 10, 2024
5daa1a4
Merge branch 'main' into louis-cqrl/add-all-endpoints-connectivity-di…
louis-cqrl Apr 11, 2024
b00f337
Add subdomain field in endpoint struct to allow other subdomains
louis-cqrl Apr 11, 2024
763f33a
Refactor endpoint structs and update connectivity diagnose tests
louis-cqrl Apr 11, 2024
6394fe9
add endpoints in endpoints.go
louis-cqrl Apr 11, 2024
bd660ed
Refactor endpoint struct and update connectivity diagnose tests
louis-cqrl Apr 12, 2024
164737f
Refactor endpoint struct and update connectivity diagnose tests
louis-cqrl Apr 12, 2024
44556b1
Crash test some endpoints
louis-cqrl Apr 13, 2024
3e50333
Refactor endpoint struct and update connectivity diagnose tests
louis-cqrl Apr 15, 2024
afad245
Make 400 with specific body message from processes endpoint work in d…
louis-cqrl Apr 17, 2024
5d44b42
Revert commented out endpoint in endpoint_info.go
louis-cqrl Apr 17, 2024
89f1c18
Merge branch 'main' into louis-cqrl/add-all-endpoints-connectivity-di…
louis-cqrl Apr 17, 2024
7cb67f8
fix linter
louis-cqrl Apr 17, 2024
3483195
fix endpoint creator
louis-cqrl Apr 17, 2024
3f2b9f1
add other subdomain test
louis-cqrl Apr 17, 2024
9f9d970
fix linter
louis-cqrl Apr 17, 2024
e542628
Merge branch 'main' into louis-cqrl/add-all-endpoints-connectivity-di…
louis-cqrl May 17, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 32 additions & 26 deletions comp/forwarder/defaultforwarder/endpoints/endpoints.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,47 +11,53 @@ import "github.com/DataDog/datadog-agent/comp/forwarder/defaultforwarder/transac

var (
// V1SeriesEndpoint is a v1 endpoint used to send series
V1SeriesEndpoint = transaction.Endpoint{Route: "/api/v1/series", Name: "series_v1"}
V1SeriesEndpoint = transaction.Endpoint{Subdomain: "", Route: "/api/v1/series", Name: "series_v1"}
// V1CheckRunsEndpoint is a v1 endpoint used to send checks results
V1CheckRunsEndpoint = transaction.Endpoint{Route: "/api/v1/check_run", Name: "check_run_v1"}
V1CheckRunsEndpoint = transaction.Endpoint{Subdomain: "", Route: "/api/v1/check_run", Name: "check_run_v1"}
// V1IntakeEndpoint is a v1 endpoint, used by Agent v.5, still used for metadata
V1IntakeEndpoint = transaction.Endpoint{Route: "/intake/", Name: "intake"}
// V1SketchSeriesEndpoint is a v1 endpoint used to send sketches
V1SketchSeriesEndpoint = transaction.Endpoint{Route: "/api/v1/sketches", Name: "sketches_v1"} //nolint unused for now
V1IntakeEndpoint = transaction.Endpoint{Subdomain: "", Route: "/intake/", Name: "intake"}
// V1ValidateEndpoint is a v1 endpoint used to validate API keys
V1ValidateEndpoint = transaction.Endpoint{Route: "/api/v1/validate", Name: "validate_v1"}
V1ValidateEndpoint = transaction.Endpoint{Subdomain: "", Route: "/api/v1/validate", Name: "validate_v1"}
// V1MetadataEndpoint is a v1 endpoint used for metadata (only used for inventory metadata for now)
V1MetadataEndpoint = transaction.Endpoint{Route: "/api/v1/metadata", Name: "metadata_v1"}

V1MetadataEndpoint = transaction.Endpoint{Subdomain: "", Route: "/api/v1/metadata", Name: "metadata_v1"}
// SeriesEndpoint is the v2 endpoint used to send series
SeriesEndpoint = transaction.Endpoint{Route: "/api/v2/series", Name: "series_v2"}
// EventsEndpoint is the v2 endpoint used to send events
EventsEndpoint = transaction.Endpoint{Route: "/api/v2/events", Name: "events_v2"}
// ServiceChecksEndpoint is the v2 endpoint used to send service checks
ServiceChecksEndpoint = transaction.Endpoint{Route: "/api/v2/service_checks", Name: "services_checks_v2"}
SeriesEndpoint = transaction.Endpoint{Subdomain: "", Route: "/api/v2/series", Name: "series_v2"}
// SketchSeriesEndpoint is the v2 endpoint used to send sketches
SketchSeriesEndpoint = transaction.Endpoint{Route: "/api/beta/sketches", Name: "sketches_v2"}
// HostMetadataEndpoint is the v2 endpoint used to send host medatada
HostMetadataEndpoint = transaction.Endpoint{Route: "/api/v2/host_metadata", Name: "host_metadata_v2"}
SketchSeriesEndpoint = transaction.Endpoint{Subdomain: "", Route: "/api/beta/sketches", Name: "sketches_v2"}

// ProcessStatusEndpoint is a v1 endpoint used to send process checks
ProcessStatusEndpoint = transaction.Endpoint{Subdomain: "process", Route: "/status", Name: "process_status"}
// ProcessesIntakeStatusEndpoint is a v1 endpoint used to send processes checks
ProcessesIntakeStatusEndpoint = transaction.Endpoint{Subdomain: "process", Route: "/intake/status", Name: "process_intake_status"}
// ProcessesEndpoint is a v1 endpoint used to send processes checks
ProcessesEndpoint = transaction.Endpoint{Route: "/api/v1/collector", Name: "process"}
ProcessesEndpoint = transaction.Endpoint{Subdomain: "process", Route: "/api/v1/collector", Name: "process"} // work with processes subdomain (get 403)
// ProcessDiscoveryEndpoint is a v1 endpoint used to sends process discovery checks
ProcessDiscoveryEndpoint = transaction.Endpoint{Route: "/api/v1/discovery", Name: "process_discovery"}
ProcessDiscoveryEndpoint = transaction.Endpoint{Subdomain: "process", Route: "/api/v1/discovery", Name: "process_discovery"} // work with processes subdomain (get 403)
// ProcessLifecycleEndpoint is a v2 endpoint used to send process lifecycle events
ProcessLifecycleEndpoint = transaction.Endpoint{Route: "/api/v2/proclcycle", Name: "process_lifecycle"}
ProcessLifecycleEndpoint = transaction.Endpoint{Subdomain: "orchestrator", Route: "/api/v2/proclcycle", Name: "process_lifecycle"} // 404 not found
// RtProcessesEndpoint is a v1 endpoint used to send real time process checks
RtProcessesEndpoint = transaction.Endpoint{Route: "/api/v1/collector", Name: "rtprocess"}
RtProcessesEndpoint = transaction.Endpoint{Subdomain: "process", Route: "/api/v1/collector", Name: "rtprocess"} // work with processes subdomain (get 403)
// ContainerEndpoint is a v1 endpoint used to send container checks
ContainerEndpoint = transaction.Endpoint{Route: "/api/v1/container", Name: "container"}
ContainerEndpoint = transaction.Endpoint{Subdomain: "process", Route: "/api/v1/container", Name: "container"} // work with processes subdomain (get 403)
// RtContainerEndpoint is a v1 endpoint used to send real time container checks
RtContainerEndpoint = transaction.Endpoint{Route: "/api/v1/container", Name: "rtcontainer"}
RtContainerEndpoint = transaction.Endpoint{Subdomain: "process", Route: "/api/v1/container", Name: "rtcontainer"}
// ConnectionsEndpoint is a v1 endpoint used to send connection checks
ConnectionsEndpoint = transaction.Endpoint{Route: "/api/v1/connections", Name: "connections"}
ConnectionsEndpoint = transaction.Endpoint{Subdomain: "process", Route: "/api/v1/connections", Name: "connections"}
// LegacyOrchestratorEndpoint is a v1 endpoint used to send orchestrator checks
LegacyOrchestratorEndpoint = transaction.Endpoint{Route: "/api/v1/orchestrator", Name: "orchestrator"}
LegacyOrchestratorEndpoint = transaction.Endpoint{Subdomain: "orchestrator", Route: "/api/v1/orchestrator", Name: "orchestrator"}
// OrchestratorEndpoint is a v2 endpoint used to send orchestrator checks
OrchestratorEndpoint = transaction.Endpoint{Route: "/api/v2/orch", Name: "orchestrator"}
OrchestratorEndpoint = transaction.Endpoint{Subdomain: "orchestrator", Route: "/api/v2/orch", Name: "orchestrator"}
// OrchestratorManifestEndpoint is a v2 endpoint used to send orchestrator manifests
OrchestratorManifestEndpoint = transaction.Endpoint{Route: "/api/v2/orchmanif", Name: "orchmanifest"}
OrchestratorManifestEndpoint = transaction.Endpoint{Subdomain: "orchestrator", Route: "/api/v2/orchmanif", Name: "orchmanifest"} // work with POST but got 401

/////////////// Unused Endpoints ///////////////

// V1SketchSeriesEndpoint is a v1 endpoint used to send sketches
V1SketchSeriesEndpoint = transaction.Endpoint{Subdomain: "", Route: "/api/v1/sketches", Name: "sketches_v1"} //nolint
// EventsEndpoint is the v2 endpoint used to send events
EventsEndpoint = transaction.Endpoint{Subdomain: "", Route: "/api/v2/events", Name: "events_v2"}
// ServiceChecksEndpoint is the v2 endpoint used to send service checks
ServiceChecksEndpoint = transaction.Endpoint{Subdomain: "", Route: "/api/v2/service_checks", Name: "services_checks_v2"}
// HostMetadataEndpoint is the v2 endpoint used to send host medatada
HostMetadataEndpoint = transaction.Endpoint{Subdomain: "", Route: "/api/v2/host_metadata", Name: "host_metadata_v2"}
)
27 changes: 27 additions & 0 deletions comp/forwarder/defaultforwarder/transaction/endpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,12 @@

package transaction

import "strings"

// Endpoint is an endpoint
type Endpoint struct {
//Subdomain of the endpoint
Subdomain string
// Route to hit in the HTTP transaction
Route string
// Name of the endpoint for the telemetry metrics
Expand All @@ -17,3 +21,26 @@ type Endpoint struct {
func (e Endpoint) String() string {
return e.Route
}

// GetEndpoint returns the full endpoint URL
func (e Endpoint) GetEndpoint(domain string) string {
if e.Subdomain == "" {
e.Subdomain = "app"
}

e.Subdomain = strings.TrimSuffix(e.Subdomain, "/")
e.Subdomain = strings.TrimPrefix(e.Subdomain, "/")

domain = strings.TrimSuffix(domain, "/")
e.Route = strings.TrimPrefix(e.Route, "/")

url := domain + "/" + e.Route

if !strings.Contains(url, "http://") && !strings.Contains(url, "https://") {
url = "https://" + url
}

url = strings.Replace(url, "app", e.Subdomain, 1)

return url
}
61 changes: 61 additions & 0 deletions comp/forwarder/defaultforwarder/transaction/endpoint_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// Unless explicitly stated otherwise all files in this repository are licensed
// under the Apache License Version 2.0.
// This product includes software developed at Datadog (https://www.datadoghq.com/).
// Copyright 2016-present Datadog, Inc.

// Package endpoints stores a collection of `transaction.Endpoint` mainly used by the forwarder package to send data to
// Datadog using the right request path for a given type of data.
package transaction

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestGetEndpoint(t *testing.T) {
tests := []struct {
name string
endpoint Endpoint
domain string
want string
}{
{
name: "default subdomain applied when empty",
endpoint: Endpoint{Route: "docs"},
domain: "https://dev.example.com",
want: "https://dev.example.com/docs",
},
{
name: "explicit subdomain replacement",
endpoint: Endpoint{Subdomain: "admin", Route: "docs"},
domain: "app.example.com",
want: "https://admin.example.com/docs",
},
{
name: "no subdomain, app not in domain",
endpoint: Endpoint{Subdomain: "app/", Route: "docs"},
domain: "myappsite.com",
want: "https://myappsite.com/docs",
},
{
name: "subdomain app with app in domain",
endpoint: Endpoint{Subdomain: "app", Route: "support"},
domain: "https://app.company.com",
want: "https://app.company.com/support",
},
{
name: "complex route and domain",
endpoint: Endpoint{Subdomain: "api", Route: "/v1/users"},
domain: "https://app.service.com",
want: "https://api.service.com/v1/users",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := tt.endpoint.GetEndpoint(tt.domain)
assert.Equal(t, tt.want, got)
})
}
}
18 changes: 18 additions & 0 deletions pkg/diagnose/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,21 @@ func diagnose(diagCfg diagnosis.Config) []diagnosis.Diagnosis {

## Context of a diagnose function execution
Normally, registered diagnose suite functions will be executed in context of the running agent service (or other services) but if ```Config.ForceLocal``` configuration is specified the registered diagnose function will be executed in the context of agent diagnose CLI command (if possible).

## Which connectivity to endpoint are tested ?
With diagnose command, the Agent try to reach out a lot of endpoints, these ones are listed below:

| Subdomain | Route | HTTP Method | Status Code expected |
|-----------|-------|-------------|----------------------|
| https://app.datadoghq.com | /api/v1/series | POST | 200 |
| https://app.datadoghq.com | /api/v1/check_run | POST | 200 |
| https://app.datadoghq.com | /intake/ | POST | 200 |
| https://app.datadoghq.com | /api/v1/validate | GET | 200 |
| https://app.datadoghq.com | /api/v1/metadata | POST | 200 |
| https://app.datadoghq.com | /api/v2/series | POST | 200 |
| https://app.datadoghq.com | /api/beta/sketches | POST | 200 |
| https://\<Agent_version>-flare.agent.datadoghq.com | /support/flare | HEAD | 307 |
| https://process.datadoghq.com | /intake/status | GET | 200 |



27 changes: 15 additions & 12 deletions pkg/diagnose/connectivity/core_endpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@
// sendHTTPRequestToEndpoint creates an URL based on the domain and the endpoint information
// then sends an HTTP Request with the method and payload inside the endpoint information
func sendHTTPRequestToEndpoint(ctx context.Context, client *http.Client, domain string, endpointInfo endpointInfo, apiKey string) (int, []byte, string, error) {
url := createEndpointURL(domain, endpointInfo)
url := endpointInfo.Endpoint.GetEndpoint(domain)
logURL := scrubber.ScrubLine(url)

// Create a request for the backend
Expand All @@ -168,18 +168,20 @@

resp, err := client.Do(req)

if err != nil {
return 0, nil, logURL, fmt.Errorf("cannot send the HTTP request to '%v' : %v", logURL, scrubber.ScrubLine(err.Error()))
}
defer func() { _ = resp.Body.Close() }()
if (err == nil) || (endpointInfo.Endpoint.Subdomain == "process" && resp.StatusCode == http.StatusBadRequest) {
defer func() { _ = resp.Body.Close() }()
// Get the endpoint response
body, err := io.ReadAll(resp.Body)
if err != nil {
return 0, nil, logURL, fmt.Errorf("fail to read the response Body: %s", scrubber.ScrubLine(err.Error()))

Check warning on line 176 in pkg/diagnose/connectivity/core_endpoint.go

View check run for this annotation

Codecov / codecov/patch

pkg/diagnose/connectivity/core_endpoint.go#L176

Added line #L176 was not covered by tests
} else if strings.Contains(string(body), "invalid message format") {
resp.StatusCode = -1
}

Check warning on line 179 in pkg/diagnose/connectivity/core_endpoint.go

View check run for this annotation

Codecov / codecov/patch

pkg/diagnose/connectivity/core_endpoint.go#L178-L179

Added lines #L178 - L179 were not covered by tests

// Get the endpoint response
body, err := io.ReadAll(resp.Body)
if err != nil {
return 0, nil, logURL, fmt.Errorf("fail to read the response Body: %s", scrubber.ScrubLine(err.Error()))
return resp.StatusCode, body, logURL, nil
}

return resp.StatusCode, body, logURL, nil
return 0, nil, logURL, fmt.Errorf("cannot send the HTTP request to '%v' : %v", logURL, scrubber.ScrubLine(err.Error()))

Check warning on line 184 in pkg/diagnose/connectivity/core_endpoint.go

View check run for this annotation

Codecov / codecov/patch

pkg/diagnose/connectivity/core_endpoint.go#L184

Added line #L184 was not covered by tests
}

// createEndpointUrl joins a domain with an endpoint
Expand Down Expand Up @@ -211,9 +213,10 @@
if statusCode >= 400 {
newErr = fmt.Errorf("bad request")
verifyReport = fmt.Sprintf("Received response : '%v'\n", scrubbedResponseBody)
}
} else if statusCode != -1 {
verifyReport += fmt.Sprintf("Received status code %v from the endpoint", statusCode)

Check warning on line 217 in pkg/diagnose/connectivity/core_endpoint.go

View check run for this annotation

Codecov / codecov/patch

pkg/diagnose/connectivity/core_endpoint.go#L216-L217

Added lines #L216 - L217 were not covered by tests

verifyReport += fmt.Sprintf("Received status code %v from the endpoint", statusCode)
}

Check warning on line 219 in pkg/diagnose/connectivity/core_endpoint.go

View check run for this annotation

Codecov / codecov/patch

pkg/diagnose/connectivity/core_endpoint.go#L219

Added line #L219 was not covered by tests
return verifyReport, newErr
}

Expand Down
9 changes: 8 additions & 1 deletion pkg/diagnose/connectivity/core_endpoint_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ var (
apiKey1 = "api_key1"
apiKey2 = "api_key2"

endpointInfoTest = endpointInfo{Endpoint: endpoints.V1ValidateEndpoint}
endpointInfoTest = endpointInfo{Endpoint: endpoints.V1ValidateEndpoint}
endpointProcessTest = endpointInfo{Endpoint: endpoints.ProcessStatusEndpoint}
)

func TestCreateEndpointUrl(t *testing.T) {
Expand Down Expand Up @@ -59,6 +60,12 @@ func TestSendHTTPRequestToEndpoint(t *testing.T) {
assert.NoError(t, err)
assert.Equal(t, statusCode, 400)
assert.Equal(t, string(responseBody), "Bad Request")

// With a different subdomain endpoint
statusCodeProcess, responseBodyProcess, _, errProcess := sendHTTPRequestToEndpoint(context.Background(), client, ts1.URL, endpointProcessTest, apiKey1)
assert.NoError(t, errProcess)
assert.Equal(t, statusCodeProcess, 200)
assert.Equal(t, string(responseBodyProcess), "OK")
}

func TestAcceptRedirection(t *testing.T) {
Expand Down
20 changes: 17 additions & 3 deletions pkg/diagnose/connectivity/endpoint_info.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,15 +41,29 @@
{endpoints.V1SeriesEndpoint, "POST", emptyPayload},
{endpoints.V1CheckRunsEndpoint, "POST", checkRunPayload},
{endpoints.V1IntakeEndpoint, "POST", emptyPayload},

// This endpoint behaves differently depending on `site` when using `emptyPayload`. Do not modify `nil` here !
{endpoints.V1ValidateEndpoint, "GET", nil},
{endpoints.V1ValidateEndpoint, "GET", nil}, // This endpoint behaves differently depending on `site` when using `emptyPayload`. Do not modify `nil` here !

Check warning on line 44 in pkg/diagnose/connectivity/endpoint_info.go

View check run for this annotation

Codecov / codecov/patch

pkg/diagnose/connectivity/endpoint_info.go#L44

Added line #L44 was not covered by tests
{endpoints.V1MetadataEndpoint, "POST", emptyPayload},

// v2 endpoints
{endpoints.SeriesEndpoint, "POST", emptyPayload},
{endpoints.SketchSeriesEndpoint, "POST", emptyPayload},

// Process endpoints
{endpoints.ProcessStatusEndpoint, "GET", nil},
{endpoints.ProcessesIntakeStatusEndpoint, "GET", nil},
{endpoints.ProcessesEndpoint, "POST", emptyPayload},
{endpoints.ProcessDiscoveryEndpoint, "POST", emptyPayload},
{endpoints.RtProcessesEndpoint, "POST", emptyPayload},
{endpoints.ContainerEndpoint, "POST", emptyPayload},
{endpoints.RtContainerEndpoint, "POST", emptyPayload},
{endpoints.ConnectionsEndpoint, "POST", emptyPayload},

// Orchestrator endpoints
//{endpoints.ProcessLifecycleEndpoint, "POST", emptyPayload},
{endpoints.LegacyOrchestratorEndpoint, "POST", emptyPayload},
{endpoints.OrchestratorEndpoint, "POST", emptyPayload},
{endpoints.OrchestratorManifestEndpoint, "POST", emptyPayload},

Check warning on line 66 in pkg/diagnose/connectivity/endpoint_info.go

View check run for this annotation

Codecov / codecov/patch

pkg/diagnose/connectivity/endpoint_info.go#L51-L66

Added lines #L51 - L66 were not covered by tests
// Flare endpoint
{transaction.Endpoint{Route: helpers.GetFlareEndpoint(cfg), Name: "flare"}, "HEAD", nil},
}
Expand Down
Loading