Skip to content

v2/logging: add support for S3 logstreaming #131

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

Merged
merged 1 commit into from
Jan 29, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
72 changes: 64 additions & 8 deletions v2/logging.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,27 +20,48 @@ const (
LogstreamCriblEndpoint LogstreamEndpointType = "cribl"
LogstreamDatadogEndpoint LogstreamEndpointType = "datadog"
LogstreamAxiomEndpoint LogstreamEndpointType = "axiom"
LogstreamS3Endpoint LogstreamEndpointType = "s3"
)

const (
LogTypeConfig LogType = "configuration"
LogTypeNetwork LogType = "network"
)

const (
S3AccessKeyAuthentication S3AuthenticationType = "accesskey"
S3RoleARNAuthentication S3AuthenticationType = "rolearn"
)

// LogstreamConfiguration type defines a log stream entity in tailscale.
type LogstreamConfiguration struct {
LogType LogType `json:"logType,omitempty"`
DestinationType LogstreamEndpointType `json:"destinationType,omitempty"`
URL string `json:"url,omitempty"`
User string `json:"user,omitempty"`
LogType LogType `json:"logType,omitempty"`
DestinationType LogstreamEndpointType `json:"destinationType,omitempty"`
URL string `json:"url,omitempty"`
User string `json:"user,omitempty"`
S3Bucket string `json:"s3Bucket,omitempty"`
S3Region string `json:"s3Region,omitempty"`
S3KeyPrefix string `json:"s3KeyPrefix,omitempty"`
S3AuthenticationType S3AuthenticationType `json:"s3AuthenticationType,omitempty"`
S3AccessKeyID string `json:"s3AccessKeyId,omitempty"`
S3RoleARN string `json:"s3RoleArn,omitempty"`
S3ExternalID string `json:"s3ExternalId,omitempty"`
}

// SetLogstreamConfigurationRequest type defines a request for setting a LogstreamConfiguration.
type SetLogstreamConfigurationRequest struct {
DestinationType LogstreamEndpointType `json:"destinationType,omitempty"`
URL string `json:"url,omitempty"`
User string `json:"user,omitempty"`
Token string `json:"token,omitempty"`
DestinationType LogstreamEndpointType `json:"destinationType,omitempty"`
URL string `json:"url,omitempty"`
User string `json:"user,omitempty"`
Token string `json:"token,omitempty"`
S3Bucket string `json:"s3Bucket,omitempty"`
S3Region string `json:"s3Region,omitempty"`
S3KeyPrefix string `json:"s3KeyPrefix,omitempty"`
S3AuthenticationType S3AuthenticationType `json:"s3AuthenticationType,omitempty"`
S3AccessKeyID string `json:"s3AccessKeyId,omitempty"`
S3SecretAccessKey string `json:"s3SecretAccessKey,omitempty"`
S3RoleARN string `json:"s3RoleArn,omitempty"`
S3ExternalID string `json:"s3ExternalId,omitempty"`
}

// LogstreamEndpointType describes the type of the endpoint.
Expand All @@ -49,6 +70,9 @@ type LogstreamEndpointType string
// LogType describes the type of logging.
type LogType string

// S3AuthenticationType describes the type of authentication used to stream logs to a LogstreamS3Endpoint.
type S3AuthenticationType string

// LogstreamConfiguration retrieves the tailnet's [LogstreamConfiguration] for the given [LogType].
func (lr *LoggingResource) LogstreamConfiguration(ctx context.Context, logType LogType) (*LogstreamConfiguration, error) {
req, err := lr.buildRequest(ctx, http.MethodGet, lr.buildTailnetURL("logging", logType, "stream"))
Expand Down Expand Up @@ -78,3 +102,35 @@ func (lr *LoggingResource) DeleteLogstreamConfiguration(ctx context.Context, log

return lr.do(req, nil)
}

// AWSExternalID represents an AWS External ID that Tailscale can use to stream logs from a
// particular Tailscale AWS account to a LogstreamS3Endpoint that uses S3RoleARNAuthentication.
type AWSExternalID struct {
ExternalID string `json:"externalId,omitempty"`
TailscaleAWSAccountID string `json:"tailscaleAwsAccountId,omitempty"`
}

// CreateOrGetAwsExternalId gets an AWS External ID that Tailscale can use to stream logs to
// a LogstreamS3Endpoint using S3RoleARNAuthentication, creating a new one for this tailnet
// when necessary.
func (lr *LoggingResource) CreateOrGetAwsExternalId(ctx context.Context, reusable bool) (*AWSExternalID, error) {
req, err := lr.buildRequest(ctx, http.MethodPost, lr.buildTailnetURL("aws-external-id"), requestBody(map[string]bool{
"reusable": reusable,
}))
if err != nil {
return nil, err
}
return body[AWSExternalID](lr, req)
}

// ValidateAWSTrustPolicy validates that Tailscale can assume your AWS IAM role with (and only
// with) the given AWS External ID.
func (lr *LoggingResource) ValidateAWSTrustPolicy(ctx context.Context, awsExternalID string, roleARN string) error {
req, err := lr.buildRequest(ctx, http.MethodPost, lr.buildTailnetURL("aws-external-id", awsExternalID, "validate-aws-trust-policy"), requestBody(map[string]string{
"roleArn": roleARN,
}))
if err != nil {
return err
}
return lr.do(req, nil)
}
59 changes: 55 additions & 4 deletions v2/logging_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,18 @@ func TestClient_SetLogstreamConfiguration(t *testing.T) {
server.ResponseCode = http.StatusOK

logstreamRequest := tsclient.SetLogstreamConfigurationRequest{
DestinationType: tsclient.LogstreamCriblEndpoint,
URL: "http://example.com",
User: "my-user",
Token: "my-token",
DestinationType: tsclient.LogstreamCriblEndpoint,
URL: "http://example.com",
User: "my-user",
Token: "my-token",
S3Bucket: "my-bucket",
S3Region: "us-west-2",
S3KeyPrefix: "logs/",
S3AuthenticationType: tsclient.S3AccessKeyAuthentication,
S3AccessKeyID: "my-access-key-id",
S3SecretAccessKey: "my-secret-access-key",
S3RoleARN: "my-role-arn",
S3ExternalID: "my-external-id",
}
server.ResponseBody = nil

Expand All @@ -64,3 +72,46 @@ func TestClient_DeleteLogstream(t *testing.T) {
assert.Equal(t, http.MethodDelete, server.Method)
assert.Equal(t, "/api/v2/tailnet/example.com/logging/configuration/stream", server.Path)
}

func TestClient_CreateOrGetAwsExternalId(t *testing.T) {
t.Parallel()

client, server := NewTestHarness(t)
server.ResponseCode = http.StatusOK

wantExternalID := &tsclient.AWSExternalID{
ExternalID: "external-id",
TailscaleAWSAccountID: "account-id",
}
server.ResponseBody = wantExternalID

gotExternalID, err := client.Logging().CreateOrGetAwsExternalId(context.Background(), true)
assert.NoError(t, err)
assert.Equal(t, server.Method, http.MethodPost)
assert.Equal(t, server.Path, "/api/v2/tailnet/example.com/aws-external-id")
assert.Equal(t, gotExternalID, wantExternalID)

gotRequest := make(map[string]bool)
err = json.Unmarshal(server.Body.Bytes(), &gotRequest)
assert.NoError(t, err)
assert.EqualValues(t, gotRequest, map[string]bool{"reusable": true})
}

func TestClient_ValidateAWSTrustPolicy(t *testing.T) {
t.Parallel()

client, server := NewTestHarness(t)
server.ResponseCode = http.StatusOK

roleARN := "arn:aws:iam::123456789012:role/example-role"

err := client.Logging().ValidateAWSTrustPolicy(context.Background(), "external-id-0000-0000", roleARN)
assert.NoError(t, err)
assert.Equal(t, server.Method, http.MethodPost)
assert.Equal(t, server.Path, "/api/v2/tailnet/example.com/aws-external-id/external-id-0000-0000/validate-aws-trust-policy")

gotRequest := make(map[string]string)
err = json.Unmarshal(server.Body.Bytes(), &gotRequest)
assert.NoError(t, err)
assert.EqualValues(t, gotRequest, map[string]string{"roleArn": roleARN})
}
Loading